进程详细讲解

2019-07-14 07:49发布

进程控制详解

在Linux中task_struct结构体即是PCB。PCB是进程的唯一标识,PCB由链表实现(为了动态插入和删除)。 进程创建时,为该进程生成一个PCB;进程终止时,回收PCB。 PCB包含信息:1、进程状态(state);2、进程标识信息(uid、gid);3、定时器(time);4、用户可见寄存器、控制状态寄存器、栈指针等(tss)   每个进程都有一个非负唯一进程ID(PID)。虽然是唯一的,但是PID可以重用,当一个进程终止后,其他进程就可以使用它的PID了。 PID为0的进程为调度进程,该进程是内核的一部分,也称为系统进程;PID为1的进程为init进程,它是一个普通的用户进程,但是以超级用户特权运行;PID为2的进程是页守护进程,负责支持虚拟存储系统的分页操作。 除了PID,每个进程还有一些其他的标识符:   五种进程状态转换如下图所示:     每个进程的task_struct和系统空间堆栈存放位置如下:两个连续的物理页【《Linux内核源代码情景分析》271页】 系统堆栈空间不能动态扩展,在设计内核、驱动程序时要避免函数嵌套太深,同时不宜使用太大太多的局部变量,因为局部变量都是存在堆栈中的。  

进程的创建

新进程的创建,首先在内存中为新进程创建一个task_struct结构,然后将父进程的task_struct内容复制其中,再修改部分数据。分配新的内核堆栈、新的PID、再将task_struct 这个node添加到链表中。所谓创建,实际上是“复制”。   子进程刚开始,内核并没有为它分配物理内存,而是以只读的方式共享父进程内存,只有当子进程写时,才复制。即“copy-on-write”。 fork都是由do_fork实现的,do_fork的简化流程如下图:

fork函数

fork函数时调用一次,返回两次。在父进程和子进程中各调用一次。子进程中返回值为0,父进程中返回值为子进程的PID。程序员可以根据返回值的不同让父进程和子进程执行不同的代码。 一个形象的过程: 运行这样一段演示程序: 复制代码 1 #include 2 #include 3 #include 4 5 int main() 6 { 7 pid_t pid; 8 char *message; 9 int n = 0; 10 pid = fork(); 11 while(1){ 12 if(pid < 0){ 13 perror("fork failed "); 14 exit(1); 15 } 16 else if(pid == 0){ 17 n--; 18 printf("child's n is:%d ",n); 19 } 20 else{ 21 n++; 22 printf("parent's n is:%d ",n); 23 } 24 sleep(1); 25 } 26 exit(0); 27 } 复制代码   运行结果: 可以发现子进程和父进程之间并没有对各自的变量产生影响。 一般来说,fork之后父、子进程执行顺序是不确定的,这取决于内核调度算法。进程之间实现同步需要进行进程通信。

什么时候使用fork呢?

一个父进程希望子进程同时执行不同的代码段,这在网络服务器中常见——父进程等待客户端的服务请求,当请求到达时,父进程调用fork,使子进程处理此请求。 一个进程要执行一个不同的程序,一般fork之后立即调用exec

vfork函数

vfork与fork对比: 相同: 返回值相同 不同: fork创建子进程,把父进程数据空间、堆和栈复制一份;vfork创建子进程,与父进程内存数据共享; vfork先保证子进程先执行,当子进程调用exit()或者exec后,父进程才往下执行 为什么需要vfork? 因为用vfork时,一般都是紧接着调用exec,所以不会访问父进程数据空间,也就不需要在把数据复制上花费时间了,因此vfork就是”为了exec而生“的。   运行这样一段演示程序: 复制代码 1 #include 2 #include 3 #include 4 5 int main() 6 { 7 pid_t pid; 8 char *message; 9 int n = 0; 10 int i; 11 pid = vfork(); 12 for(i = 0; i < 10; i++){ 13 if(pid < 0){ 14 perror("fork failed "); 15 exit(1); 16 } 17 else if(pid == 0){ 18 n--; 19 printf("child's n is:%d ",n); 20 if(i == 1) 21 _exit(0); 22 //return 0; 23 //exit(0); 24 } 25 else{ 26 n++; 27 printf("parent's n is:%d ",n); 28 } 29 sleep(1); 30 } 31 exit(0); 32 } 复制代码   运行结果: 可以发现子进程先被执行,exit后,父进程才被执行,同时子进程改变了父进程中的数据 子进程return 0 会发生什么?   运行结果: 从上面我们知道,结束子进程的调用是exit()而不是return,如果你在vfork中return了,那么,这就意味main()函数return了,注意因为函数栈父子进程共享,所以整个程序的栈就跪了。 如果你在子进程中return,那么基本是下面的过程: 1)子进程的main() 函数 return了,于是程序的函数栈发生了变化。 2)而main()函数return后,通常会调用 exit()或相似的函数(如:_exit(),exitgroup()) 3)这时,父进程收到子进程exit(),开始从vfork返回,但是尼玛,老子的栈都被你子进程给return干废掉了,你让我怎么执行?(注:栈会返回一个诡异一个栈地址,对于某些内核版本的实现,直接报“栈错误”就给跪了,然而,对于某些内核版本的实现,于是有可能会再次调用main(),于是进入了一个无限循环的结果,直到vfork 调用返回 error) 好了,现在再回到 return 和 exit,return会释放局部变量,并弹栈,回到上级函数执行。exit直接退掉。如果你用c++ 你就知道,return会调用局部对象的析构函数,exit不会。(注:exit不是系统调用,是glibc对系统调用 _exit()或_exitgroup()的封装) 可见,子进程调用exit() 没有修改函数栈,所以,父进程得以顺利执行 【《vfork挂掉的一个问题》http://coolshell.cn/articles/12103.html#more-12103

execve

可执行文件装入内核的linux_binprm结构体。 进程调用exec时,该进程执行的程序完全被替换,新的程序从main函数开始执行。因为调用exec并不创建新进程,只是替换了当前进程的代码区、数据区、堆和栈。 六种不同的exec函数: 当指定filename作为参数时: 如果filename中包含/,则将其视为路径名。 否则,就按系统的PATH环境变量,在它所指定的各个目录中搜索可执行文件。   *出于安全方面的考虑,有些人要求在搜索路径中不要包括当前目录。 在这6个函数中,只有execve是内核的系统调用。另外5个只是库函数,他们最终都要调用该系统调用,如下图所示: execve的实现由do_execve完成,简化的实现过程如下图:   关于这些函数的区别,需要时可以查看《APUE》关于exec函数部分的内容。   运行这样一段演示程序: 复制代码 1 #include 2 #include 3 #include 4 5 char command[256]; 6 void main() 7 { 8 int rtn; /*child process return value*/ 9 while(1) { 10 printf( ">" ); 11 fgets( command, 256, stdin ); 12 command[strlen(command)-1] = 0; 13 if ( fork() == 0 ) { 14 execlp( command, NULL ); 15 perror( command ); 16 exit( errno ); 17 } 18 else { 19 wait ( &rtn ); 20 printf( " child process return %d ", rtn ); 21 } 22 } 23 } 复制代码   a.out 是一个打印hello world的可执行文件。 运行结果:

进程终止

正常终止(5种)

从main返回,等效于调用exit 调用exit exit 首先调用各终止处理程序,然后按需多次调用fclose,关闭所有的打开流。 调用_exit或者_Exit 最后一个线程从其启动例程返回 最后一线程调用pthread_exit

异常终止(3种)

调用abort 接到一个信号并终止 最后一个线程对取消请求作出响应  

wait和waitpid函数

wait用于使父进程阻塞,等待子进程退出;waitpid有若干选项,如可以提供一个非阻塞版本的wait,也能实现和wait相同的功能,实际上,linux中wait的实现也是通过调用waitpid实现的。 waitpid返回值:正常返回子进程号;使用WNOHANG且没有子进程退出返回0;调用出错返回-1; 运行如下演示程序   复制代码 1 #include 2 #include 3 #include 4 #include 5 6 int main() 7 { 8 pid_t pid0,pid1; 9 pid0 = fork(); 10 if(pid0 < 0){ 11 perror("fork"); 12 exit(1); 13 } 14 else if(pid0 == 0){ 15 sleep(5); 16 exit(0);//child 17 } 18 else{ 19 do{ 20 pid1 = waitpid(pid0,NULL,WNOHANG); 21 if(pid1 == 0){ 22 printf("the child process has not exited. "); 23 sleep(1); 24 } 25 }while(pid1 == 0); 26 if(pid1 == pid0){ 27 printf("get child pid:%d",pid1); 28 exit(0); 29 } 30 else{ 31 exit(1); 32 } 33 } 34 return 0; 35 } 36 37 38 39 当把第三个参数WNOHANG改为0时,就不会有上面五个显示语句了,说明父进程阻塞了。 40 41 42 43 a.out 的代码如下: 44 45 46 #include 47 void main() 48 49 { 50 printf("hello WYJ "); 51 } 52 53 54 55 process.c的代码如下: 56 57 #include 58 #include 59 #include 60 #include 61 #include 62 #include 63 64 int main() 65 { 66 pid_t pid_1,pid_2,pid_wait; 67 pid_1 = fork(); 68 pid_2 = fork(); 69 if(pid_1 < 0){ 70 perror("fork1 failed "); 71 exit(1); 72 }else if(pid_1 == 0 && pid_2 != 0){//do not allow child 2 to excute this process. 73 if(execlp("./a.out", NULL) < 0){ 74 perror("exec failed "); 75 }//child; 76 exit(0); 77 } 78 if(pid_2 < 0){ 79 perror("fork2 failded "); 80 exit(1); 81 }else if(pid_2 == 0){ 82 sleep(10); 83 } 84 if(pid_2 > 0){//parent 85 do{ 86 pid_wait = waitpid(pid_2, NULL, WNOHANG);//no hang 87 sleep(2); 88 printf("child 2 has not exited "); 89 }while(pid_wait == 0); 90 if(pid_wait == pid_2){ 91 printf("child 2 has exited "); 92 exit(0); 93 }else{ 94 // printf("pid_2:%d ",pid_2); 95 perror("waitpid error "); 96 exit(1); 97 98 } 99 } 100 exit(0); 101 } 复制代码   运行结果:   编写一个多进程程序:该实验有 3 个进程,其中一个为父进程,其余两个是该父进程创建的子进程,其中一个子进程运行“ls -l”指令,另一个子进程在暂停 5s 之后异常退出,父进程并不阻塞自己,并等待子进程的退出信息,待收集到该信息,父进程就返回。 复制代码 1 #include 2 #include<string.h> 3 #include 4 #include 5 #include 6 #include 7 int main() 8 { 9 pid_t child1,child2,child; 10 if((child1 = fork()) < 0){ 11 perror("failed in fork 1"); 12 exit(1); 13 } 14 if((child2 = fork()) < 0){ 15 perror("failed in fork 2"); 16 exit(1); 17 } 18 if(child1 == 0){ 19 //run ls -l 20 if(child2 == 0){ 21 printf("in grandson "); 22 } 23 else if(execlp("ls", "ls", "-l", NULL) < 0){ 24 perror("child1 execlp"); 25 } 26 } 27 else if(child2 == 0){ 28 sleep(5); 29 exit(0); 30 } 31 else{ 32 do{ 33 sleep(1); 34 printf("child2 not exits "); 35 child = waitpid(child2, NULL, WNOHANG); 36 }while(child == 0); 37 if(child == child2){ 38 printf("get child2 "); 39 } 40 else{ 41 printf("Error occured "); 42 } 43 } 44 } 复制代码   运行结果:

init进程成为所有僵尸进程(孤儿进程)的父进程

僵尸进程

在进程调用了exit之后,该进程并非马上就消失掉,而是留下了一个成为僵尸进程的数据结构,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。 子进程结束之后为什么会进入僵尸状态? 因为父进程可能会取得子进程的退出状态信息。 如何查看僵尸进程? linux中命令ps,标记为Z的进程就是僵尸进程。 执行下面一段程序:   复制代码 1 #include 2 #include 3 int main() 4 { 5 pid_t pid; 6 pid = fork(); 7 if(pid < 0){ 8 printf("error occurred "); 9 }else if(pid == 0){ 10 exit(0); 11 }else{ 12 sleep(60); 13 wait(NULL); 14 } 15 } 复制代码 运行结果:    ps -ef|grep defunc可以找出僵尸进程 ps -l 可以得到更详细的进程信息 运行结果显示: 运行两次之后发现有两个Z进程,然后等待一分钟后,Z进程被父进程回收。 其中S表示状态: O:进程正在处理器运行 S:休眠状态 R:等待运行 I:空闲状态 Z:僵尸状态 T:跟踪状态 B:进程正在等待更多的内存分页 C:cpu利用率的估算值 收集僵尸进程的信息,并终结这些僵尸进程,需要我们在父进程中使用waitpid和wait,这两个函数能够手机僵尸进程留下的信息并使进程彻底消失。

守护进程Daemon

是linux的后台服务进程。它是一个生存周期较长的进程,没有控制终端,输出无处显示。用户层守护进程的父进程是init进程。 守护进程创建步骤: 1、创建子进程,父进程退出,子进程被init自动收养;fork    exit 2、调用setsid创建新会话,成为新会话的首进程,成为新进程组的组长进程,摆脱父进程继承过来的会话、进程组等;setsid 3、改变当前目录为根目录,保证工作的文件目录不被删除;chdir(“/”) 4、重设文件权限掩码,给子进程更大的权限;umask(0) 5、关闭不用的文件描述符,因为会消耗资源;close   一个守护进程的实例:每隔10s写入一个“tick”