1.1 Linux中的进程 --fork、孤儿进程、僵尸进程、文件共享分析
操作系统经典的三态如下:
1、就绪态
2、等待(阻塞)
3、运行态
其转换状态如下图所示:
操作系统内核中会维护多个队列,将不同状态的进程加入到不同的队列中,其中撤销是进程运行结束后,由内核收回。
以上的三态是操作系统原理中给出的,但是各个操作系统的平台实现这些状态的时候是有差异的,例如linux操作系统中进程的状态有以下几种:
1、运行状态(TASK_RUNNING)
2、可中断睡眠状态(TASK_INTERRUPTIBLE)
3、不可中断睡眠状态(TASK_UNINTERRUPTIBLE)
4、暂停状态(TASK_STOPPED)
5、僵死状态(TASK_ZOMBIE)
这些状态之间的转换如下所示:
linux中的进程状态是包含经典三态的,只不过分类更加细化,可中断睡眠和不可中断睡眠对应于阻塞态,就绪态也被认为是运行态的一种,也用TASK_RUNNING标识,而运行态又分为用户空间运行态和内核空间运行态,此外还多出了暂停状态和僵死状态。
小知识:
linux内核加载完成后会自己创建一个0号进程(空闲进程),创建方式不同于普通进程,然后再创建一个1号进程,其中一号进程就是/sbin/init。linux中进程的最大数量是有限的,可以通过命令 cat /proc/sys/kernel/pid_max查看,一般默认值是32768。
进程是操作系统对资源的一种抽象,一个进程包括代码段、数据段、堆栈段和进程控制块,进程控制块是操作系统管理进程的一个重要的数据结构。一个进程只能对应一个程序(这里的一个程序指的是一个可加载可执行程序。如果一个程序是一个文件,那么一个进程可以对应多个程序(文件),例如:可加载可执行程序和多个动态库程序(文件)可运行在一个进程中),一个程序可以对应多个进程。
linux中创建进程的系统调用时fork,fork函数一次调用,两次返回,子进程和父进程在各自的用户空间返回。fork一个新进程时,具体有哪些东西被生成或者拷贝了呢?在暂时不考虑COW(写时拷贝)的情况下,fork一个进程时,内核会创建一个新的PCB(进程控制块),此外还会拷贝父进程的数据段、堆栈段等。新创建的进程进入内核中的就绪队列。
查看fork的具体使用方法可以使用man命令查看。linux中的man命令共有以下几个章节:
fork失败时,只返回一次,返回值是-1,并设置全局的errno。fork成功返回时,父进程返回的是子进程的pid,这样可以让父进程知道子进程的pid,方便对子进程进行控制,父进程可能会fork很多子进程,将子进程id返回给父进程可以使父进程将这些pid保存组织起来,能轻松的对子进程进行控制,如果不是这样的话,那父进程要想找到一个子进程可能需要遍历很多复杂的数据结构,增加了复杂性。子进程从fork返回时返回的是0。父进程和子进程是一对多的关系,子进程获取父进程的pid是很方便的。
看一个fork示例:
#include <sys/types.h>
#include <unistd.h> #include <stdlib.h>
#include <stdio.h>
#include <string.h> #include <signal.h>
#include <errno.h> int main(void)
{
pid_t pid;
signal(SIGCHLD, SIG_IGN);
printf("before fork pid : %d \n", getpid()); int abc = ;
pid = fork(); if(- == pid)
{
perror("pid < 0 err");
return -;
}
if(pid > )
{
abc++;
printf("parent : pid : %d \n", getpid());
printf("abc : %d \n", abc);
sleep();
} if( == pid)
{
abc++;
printf("child : %d, parent : %d\n", getpid(), getppid());
printf("abc : %d \n", abc);
}
return ;
}
运行结果如下:
可以看到成功创建了子进程,而且父进程和子进程各有一份abc变量的副本。
再看下一个fork小程序:
#include <unistd.h>
#include <stdio.h> int main()
{
fork();
fork();
fork(); printf("Hello fork ...\n");
return ;
}
这个程序中fork“分裂”的结果如下所示:
因此,最后会打印出8条语句,如下所示:
如果想让一个父进程按如下图所示的方式产生多个子进程该怎么办呢?
直接给出如下的程序:
#include <sys/types.h>
#include <unistd.h> #include <stdlib.h>
#include <stdio.h>
#include <string.h> #include <signal.h>
#include <errno.h> void TestFunc(int num)
{
printf("TestFunc : %d\n", num);
} int main(void)
{
int i = ;
int j = ; int ProcNum = ;
int LoopNum = ; printf("please enter the ProcNum : \n");
scanf("%d", &ProcNum); printf("please enter the LoopNum : \n");
scanf("%d", &LoopNum); pid_t pid; for(i = ; i < ProcNum; i++)
{
pid = fork(); if( == pid)
{
for(j = ; j < LoopNum; j++)
{
TestFunc(j);
} exit();
}
} printf("parent process exit\n");
return ;
}
运行时,我们输入产生的进程数和每个进程循环的次数,执行结果如下:
写时拷贝:
父进程fork出子进程后,这时两个进程有各自的虚拟地址空间,但是它们相同的虚拟地址空间映射到了同一片物理内存中的代码段、数据段、堆栈段,也就是说它们是 共享物理内存中的数据的,只有当子进程或者父进程试图去修改这些物理内存中的数据时,才会触发缺页异常,完成真正的物理内存的拷贝,并修改进程相应的页表,而这个真正的拷贝也不是拷贝所有物理内存,而是拷贝进程真正修改的位置所在的页框,等到下次再访问到其他页框是再去拷贝其他页框。
孤儿进程和僵尸进程:
- 如果父进程先退出,子进程还没有退出,这时子进程就成了孤儿进程,子进程的父进程会被内核设置成1号进程即init进程(注:任何一个进程都必须有父进程)。
- 如果子进程先退出,父进程还没有退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵尸进程。
孤儿进程实验,程序如下:
#include <sys/types.h>
#include <unistd.h> #include <stdlib.h>
#include <stdio.h>
#include <string.h> #include <signal.h>
#include <errno.h> int main(void)
{
pid_t pid;
printf("before fork pid : %d \n", getpid()); pid = fork(); if(- == pid)
{
perror("pid < 0 err");
return -;
}
if(pid > )
{
printf("parent : pid : %d \n", getpid());
} if( == pid)
{
printf("child : %d, parent : %d\n", getpid(), getppid());
sleep();
}
return ;
}
上面程序中让父进程先死,子进程睡眠100秒后死,执行结果如下:
可见,a.out进程的父进程变成了1号进程。
下面演示僵尸进程,程序如下:
#include <sys/types.h>
#include <unistd.h> #include <stdlib.h>
#include <stdio.h>
#include <string.h> #include <signal.h>
#include <errno.h> int main(void)
{
pid_t pid;
printf("before fork pid : %d \n", getpid()); pid = fork(); if(- == pid)
{
perror("pid < 0 err");
return -;
}
if(pid > )
{
printf("parent : pid : %d \n", getpid());
sleep();
} if( == pid)
{
printf("child : %d, parent : %d\n", getpid(), getppid());
}
return ;
}
让子进程先死,父进程睡眠20秒,先不收尸,执行结果如下:
图中显示子进程4003处于defunct状态,也即僵尸状态。
在程序中我们应该怎么避免僵尸进程呢?那就是在创建子进程的时候,我们要告诉内核,子进程结束时,我们不准备收尸,这样的话内核就会来完成这件事。告诉内核不收尸这件事是通过signal函数来完成的,在调用fork之前,我们使用signal(SIGCHLD, SIG_IGN)告诉内核不收尸,子进程死了之后,内核会向其父进程发送SIGCHID信号,而这个调用的含义就是告诉内核:父进程忽略SIGCHLD这个信号,SIG_IGN是忽略的意思,这句话同时也是告诉内核让内核来收尸。
更改程序如下:
#include <sys/types.h>
#include <unistd.h> #include <stdlib.h>
#include <stdio.h>
#include <string.h> #include <signal.h>
#include <errno.h> int main(void)
{
pid_t pid;
printf("before fork pid : %d \n", getpid()); signal(SIGCHLD, SIG_IGN);
pid = fork(); if(- == pid)
{
perror("pid < 0 err");
return -;
}
if(pid > )
{
printf("parent : pid : %d \n", getpid());
sleep();
} if( == pid)
{
printf("child : %d, parent : %d\n", getpid(), getppid());
}
return ;
}
以上程序只是添加了第16行,运行结果如下:
如果想知道系统支持哪些信号可以使用kill -l命令,如下:
进程间的文件共享:
父进程打开一个文件,然后调用了fork,文件描述符会复制给子进程吗?两个进程同时读写文件会互相影响吗?下面我们一一进行分析,首先给出打开文件的一个测试程序,如下:
#include <sys/types.h>
#include <unistd.h> #include <stdlib.h>
#include <stdio.h>
#include <string.h> #include <signal.h>
#include <errno.h> #include <sys/stat.h>
#include <fcntl.h> int main(void)
{
pid_t pid;
int fd = ; printf("before fork pid : %d \n", getpid());
signal(SIGCHLD, SIG_IGN); fd = open("./1.txt", O_RDWR);
if(fd == -)
{
perror("open");
return -;
} pid = fork(); if(- == pid)
{
perror("pid < 0 err");
return -;
}
if(pid > )
{
printf("parent : pid : %d \n", getpid());
sleep();
} if( == pid)
{
printf("child : %d, parent : %d\n", getpid(), getppid());
}
return ;
}
第23行,我们打开当前目录下的1.txt,当这个文件不存在时,open返回-1,我们使用perror打印返回-1时的错误信息,执行程序,输出如下:
执行上述程序时,1.txt不存在,open返回了-1,出错信息的那一行中open字符串使,是我们传给perror函数的,其余的信息是perror根据error出错号打印的信息。我们在调用perror时,只需要给它传入一个“标题“即可,在本例中我们传入的标题即是“open”。我们在当前目录创建1.txt后再次执行程序就不会出错了。
我们在父进程和子进程中同时向文件中写数据,程序如下:
#include <sys/types.h>
#include <unistd.h> #include <stdlib.h>
#include <stdio.h>
#include <string.h> #include <signal.h>
#include <errno.h> #include <sys/stat.h>
#include <fcntl.h> int main(void)
{
pid_t pid;
int fd = ; printf("before fork pid : %d \n", getpid());
signal(SIGCHLD, SIG_IGN); fd = open("./1.txt", O_RDWR);
if(fd == -)
{
perror("open");
return -;
} pid = fork(); if(- == pid)
{
perror("pid < 0 err");
return -;
}
if(pid > )
{
printf("parent : pid : %d \n", getpid());
write(fd, "parent", );
} if( == pid)
{
printf("child : %d, parent : %d\n", getpid(), getppid());
write(fd, "child", );
} return ;
}
运行结果如下:
从上图中可以看到多次运行的结果都不一样,可见父进程和子进程写数据的顺序是不一定的,也有可能是交叉写入的,会相互影响。父进程在fork时将文件描述符复制给了子进程。要想关闭一个这个文件,必须在父进程和子进程中都执行一次close操作才会真正将文件关闭。
父子进程共享文件的示意图如下:
每一个进程中会有一个 文件描述符表,表中的每一项都是一个指向文件表的指针,文件描述符fd只是文件描述符表的下标。文件表是真正描述一个文件状态的数据结构,从上图可以看出,虽然父进程和子进程各有一份文件描述符表,但是它们是共享同一个文件表的,而且文件表中有一项是引用计数,表示有几个进程正在引用它,这些共享文件表的进程当然也是共享当前文件偏移量的,所以它们在写入数据时,会发生顺序错乱,但是不会覆盖。
多个进程打开一个文件会对应多个不同的文件表,这时候文件表不再是共享的,它们维护各自的偏移量,但是多个文件表最终还是指向相同的inode,因为是同一个物理文件。同一个进程打开一个文件多次,也不会共享同一个文件表,而是每个文件描述符各自对应一个文件表。使用fork产生的父子进程之间会共享同一个文件表。
1.1 Linux中的进程 --fork、孤儿进程、僵尸进程、文件共享分析的更多相关文章
- linux多进/线程编程(3)——wait、waitpid函数和孤儿、僵尸进程
当使用fork创建多个进程后,需要解决子进程回收的问题.wait和waitpid函数就是做这个工作的. 假设子进程没有合理的回收,可能会带来两个问题: 1.孤儿进程(父进程挂了,子进程活着),孤儿进程 ...
- 如何在 Linux 中找出 CPU 占用高的进程
1) 怎样使用 top 命令找出 Linux 中 CPU 占用高的进程 在所有监控 Linux 系统性能的工具中,Linux 的 top 命令是最好的也是最知名的一个.top 命令提供了 Linux ...
- 在 Linux 中找出 CPU 占用高的进程
列出系统中 CPU 占用高的进程列表来确定.我认为只有两种方法能实现:使用 top 命令 和 ps 命令.出于一些理由,我更倾向于用 top 命令而不是 ps 命令.但是两个工具都能达到你要的目的,所 ...
- 在 Linux 中找出内存消耗最大的进程
1 使用 ps 命令在 Linux 中查找内存消耗最大的进程 ps 命令用于报告当前进程的快照.ps 命令的意思是"进程状态".这是一个标准的 Linux 应用程序,用于查找有关在 ...
- 2次使用fork避免产生僵尸进程和不去处理SIGCHLD信号
1.如下代码所示 #include <unistd.h> #include <sys/types.h> #include <unistd.h> int main(i ...
- 查找Linux中内存和CPU使用率最高的进程
下面的命令会查看到按照RAM和CPU降序方式的前最高几名进程的列表: [root@iZ25pvjcsyhZ ~]# ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%mem ...
- 对linux中source,fork,exec的理解以及case的 使用
fork 使用 fork 方式运行 script 时, 就是让 shell(parent process) 产生一个 child process 去执行该 script, 当 child proc ...
- linux中exec和xargs命令的区别和优劣分析
find的exec及ok命令 exec命令的格式为: exec command {} \; exec后面跟着的是操作命令,然后跟着{}表示每一个参数,然后空格,然后"\;".{}之 ...
- 转载 linux 僵尸进程,讲的很透彻
僵尸进程的产生和避免,以及wait,waitpid的使用 在fork()/execve()过程中,假设子进程结束时父进程仍存在,而父进程fork()之前既没安装SIGCHLD信号处理函数调用waitp ...
随机推荐
- Ubuntu 14.04 vi 退格键不能删除字符
执行命令 sudo apt-get install vim
- js获取url 参数
window.getRequest = function (url) { var theRequest = new Object(); var indexOf = url.indexOf(" ...
- Codeforces 496C - Removing Columns
496C - Removing Columns 思路:暴力,用vis标记数组实时记录一下之前的行i+1和上一行i否全相等,false表示全相等. 代码: #include<bits/stdc++ ...
- C++ 多态性和虚函数
2017-06-27 19:17:52 C++面向对象编程的一个重要的特性就是多态性,而多态性的实现需要依赖虚函数的帮助. 一.多态的作用: 隐藏实现细节,使得代码能够模块化: 接口重用,实现“一个接 ...
- Python 爬虫-信息的标记xml,json,yaml
2017-07-26 23:53:03 信息标记的作用有: 标记后的信息可形成信息组织结构,增加了信息维度 标记的结构与信息一样具有重要价值 标记后的信息可用于通信.存储或展示 标记后的信息更利于程 ...
- Confluence 6 快捷键
快捷键图标. 官方的下载地址为:https://atlassianblog.wpengine.com/wp-content/uploads/2018/01/keyboard-shortcuts-inf ...
- Ciel the Commander CodeForces - 321C (树, 思维)
链接 大意: 给定n结点树, 求构造一种染色方案, 使得每个点颜色在[A,Z], 且端点同色的链中至少存在一点颜色大于端点 (A为最大颜色) 直接点分治即可, 因为最坏可以涂$2^{26}-1$个节点 ...
- Neo4j视频教程 Neo4j 图数据库视频教程
课程名称 课程发布地址 地址: 腾讯课堂<Neo4j 图数据库视频教程> https://ke.qq.com/course/327374?tuin=442d3e14 作者 庞国明,< ...
- Apache Spark探秘:利用Intellij IDEA构建开发环境
1)准备工作 1) 安装JDK 6或者JDK 7 或者JDK8 mac 的 参看http://docs.oracle.com/javase/8/docs/technotes/guide ...
- OncePerRequestFilter的作用
在spring中,filter都默认继承OncePerRequestFilter,但为什么要这样呢? OncePerRequestFilter顾名思义,他能够确保在一次请求只通过一次filter,而不 ...