Linux系统:进程控制

Posted Fox!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux系统:进程控制相关的知识,希望对你有一定的参考价值。

目录

1 进程创建

1.1 fork函数

1.2 写时拷贝

1.3 fork常规用法

1.4 fork调用失败的原因

 2 进程终止

2.1 进程退出场景

2.2 进程常见退出方法

 3 进程等待

3.1 进程等待必要性

 3.2 进程等待的方法

3.2.1 wait方法

3.2.2 waitpid方法

 3.3 获取子进程status

4 进程程序替换

4.1 替换原理

4.2 替换函数

4.3 函数解释

4.4 命名理解 


1 进程创建

1.1 fork函数

linuxfork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。这个我们在之前已经讲过了,这里就不再多说了。

需要补充一下的是:

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度
当一个进程调用 fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程。 fork 之前父进程独立执行, fork 之后,父子两个执行流分别执行。注意, fork 之后,谁先执行完全由调度器 决定。

1.2 写时拷贝

这个我们在进程地址空间那里做了很详细的讲解,这里就不再多说了。

1.3 fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

1.4 fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

 2 进程终止

2.1 进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

 第一种与第二种是程序已经正常执行完毕,是通过退出码拿到结果。而第二种是代码已经崩溃了需要接受操作系统发送的信号。(这个我们后面讲信号时会详细讲解)

2.2 进程常见退出方法

正常终止(可以通过 echo $? 查看进程退出码):

  •  main返回
  •  调用exit
  •  _exit
异常退出:
  • ctrl + c,信号终止

我们可以来试试echo $?的基本用法:

[grm@VM-8-12-centos lesson9]$ ls -la
total 28
drwxrwxr-x  2 grm grm 4096 Mar  8 23:01 .
drwx------ 18 grm grm 4096 Mar  8 22:57 ..
-rw-rw-r--  1 grm grm   64 Mar  8 22:59 Makefile
-rwxrwxr-x  1 grm grm 8304 Mar  8 23:01 mytest
-rw-rw-r--  1 grm grm   66 Mar  8 23:01 test.c
[grm@VM-8-12-centos lesson9]$ echo $?
0
[grm@VM-8-12-centos lesson9]$ ls -lz
ls: invalid option -- 'z'
Try 'ls --help' for more information.
[grm@VM-8-12-centos lesson9]$ echo $?
2

我们发现当我们使用正确的ls指令时,用echo $?返回的退出码是0,而输出错误的指令得到的退出码是2。

我们创建一个test.c:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 
  4 int main()
  5 
  6 
  7   return 2;                                                                                                                            
  8 

当我们这样运行时:

[grm@VM-8-12-centos lesson9]$ vim test.c
[grm@VM-8-12-centos lesson9]$ make
gcc -o mytest test.c
[grm@VM-8-12-centos lesson9]$ ./mytest
[grm@VM-8-12-centos lesson9]$ echo $?
2
[grm@VM-8-12-centos lesson9]$ echo $?
0

我们运行时第一次用$?得到的退出码与我们预估计的一样,但是当我们再次echo $?时却已经不是我们想要的结果了,这是因为$?只会保留最近的一次退出码。

当我们想要结束进程外除了用return我们还可以用exit来结束进程。但是除了exit,还提供了_exit函数,他们有什么区别吗?我们可以用代码来试试。

当我们使用exit时:

  1 #include<stdio.h>
  2 #include<stdlib.h>                                                                                                                      
  3 #include<unistd.h>
  4 
  5 int main()
  6 
  7   printf("hello exit");
  8   exit(1);
  9   return 0;
 10 

运行结果:

 当我们使用_exit时:

  1 #include<stdio.h>
  2 #include<stdlib.h>                                                                                                                      
  3 #include<unistd.h>
  4 
  5 int main()
  6 
  7   printf("hello exit");
  8   _exit(1);
  9   return 0;
 10 

运行结果:

 我们发现_exit是不会刷新行缓冲区的,但是exit是会的。

exit 最后也会调用 _exit, 但在调用 _exit 之前,还做了其他工作:
1. 执行用户通过 exit _exit 定义的清理函数。 2. 关闭所有打开的流,所有的缓存数据均被写入 3. 调用 _exit

 


 3 进程等待

3.1 进程等待必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。进程一旦变成僵尸状态,那就刀枪不入,杀人不眨眼kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 父进程派给子进程的任务完成的如何,我们需要知道。如子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

 3.2 进程等待的方法

3.2.1 wait方法

#include<sys/types.h> #include<sys/wait.h> pid_t wait(int*status); 返回值: 成功返回被等待进程pid,失败返回-1 参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。

 这个方法很简单,我就不试试了,重点是下面的waitpid这个方法。

3.2.2 waitpid方法

pid_ t waitpid(pid_t pid, int *status, int options); 返回值: 当正常返回的时候waitpid返回收集到的子进程的进程ID 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在; 参数: pid Pid=-1,等待任一个子进程。与wait等效。 Pid>0.等待其进程IDpid相等的子进程。 status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): WIFEXITED非零,提取子进程退出码。(查看进程的退出码) options: WNOHANG: pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID

 我们来看看是怎么用的:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 int main()
  7 
  8   pid_t id=fork();
  9   if(id==0)
 10   
 11     //child
 12     int cnt=5;
 13     while(cnt--)
 14     
 15       printf("我是一个子进程,我的pid:%d ,ppid:%d \\n",getpid(),getppid());
 16       sleep(1);
 17     
 18     exit(0);
 19   
 20   //parent
 21   int status=0;                                                                                                                                            
 22   int ret_pid=waitpid(id,&status,0);
 23   printf("我是一个父进程,我的pid:%d\\n",getpid());
 24   if(ret_pid<0)
 25   
 26     printf("wait error\\n");
 27   
 28   else if(ret_pid>0)
 29   
 30     printf("wait success\\n");
 31   
 32   return 0;
 33 
~
~

我们来看看运行结果:

 那父进程想要拿到子进程的状态应该怎么办呢?我们可以用status来获得。

当然用宏也能够得到,并且更加容易理解。下面我们这里用宏来获得:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 int main()
  7 
  8   pid_t id=fork();
  9   if(id==0)
 10   
 11     //child
 12     int cnt=10;
 13     while(cnt)
 14     
 15       printf("我是一个子进程,我还活着呢,cnt:%d,我的pid:%d ,ppid:%d \\n",cnt--,getpid(),getppid());
 16       sleep(1);
 17     
 18     exit(0);
 19   
 20   //parent
 21   while(1)
 22   
 23     int status=0;
 24     int ret_pid=waitpid(id,&status,WNOHANG);
 25     //bootprintf("我是一个父进程,我的pid:%d\\n",getpid());
 26     if(ret_pid<0)                                                                                                                                          
 27     
 28       printf("wait error\\n");
 29       exit(1);
 30     
 31     else if(ret_pid>0)
 32     
 33       //printf("wait success\\n");
 34       if(WIFEXITED(status))
 35       
 36         printf("wait success 退出码为:%d\\n",WEXITSTATUS(status));
 37      
 38       else
 39       
 40         printf("wait success 退出信号为:%d\\n",status&0x7f);
 41       
 42       break;
 43     
 44     else 
 45     
 46       printf("我是一个父进程,我再做其他的事情\\n");
 47       sleep(1);
 48       continue;
 49     
 50    
 51   return 0;
 52 

当我们运行时:

 假如我们想让父进程收到信号呢?我们可以用kill -9命令来杀掉子进程。

 这样我们就得到了退出信号。

大家这时或许会疑问:这个WNOHANG又是什么鬼呢?

我们之前设置的默认参数为0,表示一种阻塞等待,就是父进程在等待子进程结束时不会去做其他的事情,而是专心等待,这里把参数设置成了WNOHANG就是不让其阻塞等待,而在等待子进程死亡时做一些其他的事情。平时我们说卡住了也喜欢说成HANG住了。

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

 3.3 获取子进程status

  • waitwaitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status16比特位)

 这个我们之前已经讲过了,获取终止信号用的是status&0x7f,获取退出状态可以用(status>>8)&0xff,也可以用上面我们讲到的宏(WEXITSTATUS)来处理。

但是在前面进程非阻塞代码那里我们只是用了一个打印语句来执行父进程在做其他的事情,如何写的更加真实漂亮一些呢?我们这里可以给出一些补充仅供参考:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  5 #include<sys/wait.h>
  6 //预设置一些任务
  7 #define TASK_NUM 10
  8 void sync_disk()
  9 
 10   printf("这是一个数据刷新的任务\\n");
 11 
 12 
 13 void sync_log()
 14 
 15   printf("这是一个同步日志的任务\\n");
 16 
 17 
 18 void net_sent()
 19 
 20   printf("这是一个网络发送的任务\\n");
 21 
 22 
 23 typedef void (*fun)();                                                                                                  
 24 
 25 fun other_task[TASK_NUM]=NULL;
 26 
 27 int load_disk(fun f)
 28 
 29   int i=0;
 30   for(;i<TASK_NUM;i++)
 31   
 32     if(other_task[i]==NULL) break;
 33   
 34   if(i==TASK_NUM) return -1;
 35   other_task[i]=f;
 36   return 0;                                                                                                             
 37 
 38 
 39 void init_task()
 40 
 41 
 42   int i=0;
 43   for(;i<TASK_NUM;i++) other_task[i]=NULL;
 44   load_disk(sync_log);
 45   load_disk(sync_disk);
 46   load_disk(net_sent);
 47 
 48 
 49 void run_task()
 50 
 51   int i=0;
 52   for(;i<TASK_NUM;i++)
 53   
 54     if(other_task[i]==NULL) continue;                                                                                   
 55     else other_task[i]();
 56   
 57 
 58 int main()
 59 
 60   pid_t id=fork();
 61   if(id==0)
 62   
 63     //child
 64     int cnt=5;
 65     while(cnt)
 66     
 67       printf("我是一个子进程,我还活着呢,cnt:%d,我的pid:%d ,ppid:%d \\n",cnt--,getpid(),getppid());
 68       sleep(1);
 69     
 70     exit(0);
 71   
 72   //parent
 73   init_task();
 74   while(1)
 75   
 76     int status=0;
 77     int ret_pid=waitpid(id,&status,WNOHANG);
 78     //bootprintf("我是一个父进程,我的pid:%d\\n",getpid());
 79     if(ret_pid<0)
 80     
 81       printf("wait error\\n");                                                                                           
 82       exit(1);
 83     
 84     else if(ret_pid>0)
 85     
 86       //printf("wait success\\n");
 87       if(WIFEXITED(status))
 88       
 89         printf("wait success 退出码为:%d\\n",WEXITSTATUS(status));
 90      
 91       else
 92       
 93         printf("wait success 退出信号为:%d\\n",status&0x7f);
 94       
 95       break;
 96     
 97     else 
 98     
 99       //printf("我是一个父进程,我再做其他的事情\\n");
100       run_task();
101       sleep(1);
102       continue;
103     
104    
105   return 0;
106 

运行结果:


4 进程程序替换

4.1 替换原理

fork 创建子进程后执行的是和父进程相同的程序 ( 但有可能执行不同的代码分支 ), 子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时 , 该进程的用户空间代码和数据完全被新程序替换 , 从新程序的启动 例程开始执行。调用 exec 并不创建新进程 , 所以调用 exec 前后该进程的 id 并未改变。

 我们之前说过当fork之后父子进程的代码是共享的,数据会通过写时拷贝来进行处理,但是现在我们可以通过调用exec函数来帮助我们替换代码,话不多说,我们先来简单的认识一下:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 
  4 int main()
  5 
  6   printf("begin:       !!!\\n");
  7   printf("begin:       !!!\\n");
  8   printf("begin:       !!!\\n");
  9   printf("begin:       !!!\\n");
 10   printf("我是一个进程:pid():%d\\n",getpid());
 11   execl("/bin/ls","ls","-l","-a",NULL);                                                                                 
 12   printf("end:         !!!\\n");
 13   printf("end:         !!!\\n");
 14   printf("end:         !!!\\n");
 15   printf("end:         !!!\\n");
 16   return 0;
 17 

我们运行一下:

 不难发现程序中有begin,但是很奇怪没有end,这就是因为程序代码在execl之后就被替换了,所以后面的代码根本就不会执行。

4.2 替换函数

像与execl这种函数类似的还有另外的几个,我们一个一个来看:

#include <unistd.h>

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

首先来介绍第一个函数execl,第一个参数是路径,就是我们要执行的进程的绝对路径,后面的参数我们之前在命令行上怎样敲的命令就用""将我们的命令分割出来,最后在用NULL结束

第4个函数execv,第一个参数与execl一样,写出该进程的绝对路径,第二个参数是一个指针数组类型,我们将在命令行上写的命令放在该数组中就行。

我们再来看看第二个函数execlp,第一个参数是执行进程的名称,ps(这里不需要写绝对路径,由于系统会自动在环境变量PATH中查找),第二个参数与execl第二个参数一样。

再来看看第三个函数execle,在讲这个函数之前我们试试能否用一个程序调用另外一个程序:

我们创建一个c++文件:

  1 #include<iostream>                                                                                                      
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 using namespace std;
  5 
  6 int main()
  7 
  8   cout<<"我是另一个程序,我的pid是:"<<getpid()<<endl;
  9   cout<<"MYENV: "<<(getenv("MYENV")==NULL?"NULL":getenv("MYENV"))<<endl;
 10 
 11   return 0;
 12 

当我们运行时:

可是当我们在test.c中调用该程序时:

 可见环境变量已经继承给子进程了。

test.c:

  #include<stdio.h>
  #include<stdlib.h>
  #include<unistd.h>
  #include<sys/types.h>
  #include<sys/wait.h>
  
  int main()
  
    extern char** environ;
    pid_t id=fork();
    if(id==0)
    
      //child
      printf("我是子进程,我的pid为:%d\\n",getpid());
      char* const myenv[]=
        "MYENV=YouCanSeeMe",
        NULL
      ;
      execle("../enc/other","other",NULL,myenv);
      exit(1);
    
  
    //parent
    printf("我是父进程,我的pid是:%d\\n",getpid());                                                                                                            
    int status=0;
    pid_t pid=waitpid(id,&status,0);
    if(pid<0)
    
      printf("wait error\\n");
    
    else
    
      if(WIFEXITED(status))
      
        printf("wait success,exit code:%d\\n",WEXITSTATUS(status));
      
      else
        printf("wait success,singal:%d\\n",status&0x7f);
      
   
    return 0;
  

 但是当我们修改other.cc代码:

    1 #include<iostream>
    2 #include<unistd.h>
    3 #include<stdlib.h>
    4 using namespace std;
    5 
    6 int main()
    7 
    8   cout<<"我是另一个程序,我的pid是:"<<getpid()<<endl;
    9   cout<<"MYENV: "<<(getenv("MYENV")==NULL?"NULL":getenv("MYENV"))<<endl;
E> 10   cout<<"PATH: "<<(getenv("PATH")==NULL?"NULL":getenv("PATH"))<<endl;                                                  
   11   return 0;
   12 

 

 我们发现当我们用test.c调用other.cc程序时添加的环境变量居然把PATH给覆盖了。有什么方法可以解决吗?

答案是有的,我们在test.c中可以通过putenv函数将环境变量导入到environ中,我们使用environ将环境变量传给被调用的程序。

test.c:

  #include<stdio.h>
  #include<stdlib.h>
  #include<unistd.h>
  #include<sys/types.h>
  #include<sys/wait.h>
  
  int main()
  
    extern char** environ;
    pid_t id=fork();
    if(id==0)
    
      //child
      printf("我是子进程,我的pid为:%d\\n",getpid());
     /* char* const myenv[]=
        "MYENV=YouCanSeeMe",
        NULL
      ;*/
      putenv("MYENV=YouCanSeeMe");
      execle("../enc/other","other",NULL,environ);
      exit(1);
    
  
    //parent                                                                                                                                                   
    printf("我是父进程,我的pid是:%d\\n",getpid());
    int status=0;
    pid_t pid=waitpid(id,&status,0);
    if(pid<0)
    
      printf("wait error\\n");
    
    else
    
      if(WIFEXITED(status))
      
        printf("wait success,exit code:%d\\n",WEXITSTATUS(status));
      
      else
        printf("wait success,singal:%d\\n",status&0x7f);
      
    
    return 0;
  

当我们运行时:

 我们通过上面的栗子来思考下为什么环境变量具有全局属性(子进程为什么会继承父进程的环境变量),也就是我们究竟是通过什么方式办到的?

相信大家此时已经有了答案,我们是通过exec函数将环境变量通过参数传递过去的。

那我们再思考一下,我们在test.c中不通过putenv来获得设置变量,而是在命令行上通过export来导入环境变量,这样可行吗?

我们分析分析,当我们export导入环境变量是导入在bash中的,而bash是我们创建的test.c的父进程,而子进程会继承父进程的环境变量,所以bash就将它的环境变量传递给了它的孙子进程,故是可以的,我们来验证验证:

 结果与我们预想的一样。(ps:此时在test.c中我们是屏蔽了putenv的)

4.3 函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。

4.4 命名理解 

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量
函数名参数格式是否带路径是否使用当前环境变量
execl列表
execlp列表
execv数组
execvp数组
execle列表否,需要自己组装环境变量
execve数组否,需要自己组装环境变量

 表格中需要大家注意的是:是否带路径表示的是能不能去环境变量PATH中找,若带了路径,说明在环境变量中能够找到就不需要写出绝对路径,只写要执行文件的名字就好了,若没有带路径则说明需要自己补全路径来找寻该文件的位置。

事实上 , 只有 execve 是真正的系统调用 , 其它五个函数最终都调用 execve, 所以 execve man 手册 第 2 , 其它函数在man 手册第 3 节,其他的函数都是execve的封装。

Linux系统编程之进程控制

1.进程创建

在上一节讲解进程概念时,我们提到fork函数是从已经存在的进程中创建一个新进程。那么,系统是如何创建一个新进程的呢?这就需要我们更深入的剖析fork函数。

1.1 fork函数的返回值

调用fork创建进程时,原进程为父进程,新进程为子进程。运行man fork后,我们可以看到如下信息:

#include <unistd.h>
pid_t fork(void);

fork函数有两个返回值,子进程中返回0,父进程返回子进程pid,如果创建失败则返回-1。

实际上,当我们调用fork后,系统内核将会做:

  • 分配新的内存块和内核数据结构(如task_struct)给子进程
  • 将父进程的部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表中
  • fork返回,开始调度

1.2 写时拷贝

在创建进程的过程中,默认情况下,父子进程共享代码,但是数据是各自私有一份的。如果父子只需要对数据进行读取,那么大多数的数据是不需要私有的。这里有三点需要注意:

第一,为什么子进程也会从fork之后开始执行?

因为父子进程是共享代码的,在给子进程创建PCB时,子进程PCB中的大多数数据是父进程的拷贝,这里面就包括了程序计数器(PC)。由于PC中的数据是即将执行的下一条指令的地址,所以当fork返回之后,子进程会和父进程一样,都执行fork之后的代码。

第二,创建进程时,子进程需要拷贝父进程所有的数据吗?

父进程的数据有很多,但并不是所有的数据都要立马使用,因此并不是所有的数据都进行拷贝。一般情况下,只有当父进程或者子进程对某些数据进行写操作时,操作系统才会从内存中申请内存块,将新的数据拷写入申请的内存块中,并且更改页表对应的页表项,这就是写时拷贝。原理如下图所示:

第三,为什么数据要各自私有?

这是因为进程具有独立性,每个进程的运行不能干扰彼此。

1.3 fork函数的用法及其调用失败的原因

fork函数的用法:

  • 一个父进程希望复制自己,通过条件判断,使父子进程分流同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求。
  • 如子进程从fork返回后,调用进程替换的函数,如exec等(将会在本节4.程序替换中讲解)。

fork函数调用失败的原因:

  • 系统中进程太多
  • 实际用户的进程数超过了限制

2.进程终止

2.1 进程终止的原因

进程终止的原因有三种

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

2.2 常见的进程退出方法

进程正常终止

1.从main函数return,这是最常见的进程退出方法。在函数设计中,0代表正确,非0代表错误。其中不同的非0的退出码对应了退出原因。

2.调用exit或者_exit

_exit函数是系统调用,执行man _exit可以看到

#include <unistd.h>
void _exit(int status);

status 定义了进程的终止状态。父进程可以通过wait来获得子进程的status(会在3.进程等待中讲解)。

需要注意的是,exit函数是库函数,虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行echo $?发现返回值是255。

#include <stdlib.h>
void exit(int status);

从作用上来看,_exit和exit是相似的,exit是对_exit的封装,exit的执行实际上是通过调用_exit来实现的。

但是二者也有一些细微的差别,请看如下代码段:

代码1

int main()
{
 printf("Hello world");
 exit(0);
}

代码2

 #include<stdio.h>  
 #include<unistd.h>  
 int main()  
 {  
   printf("Hello world");  
    _exit(0);  

相比于_exit函数,exit函数先要执行用户定义的清理函数,在冲刷缓冲区,关闭所有打开的流,将所有的缓存数据写入文件后,再调用_exit。因此我们可以看到,执行exit输出了“hello World",而执行_exit并没有输出。

那么,return和exit有什么区别呢?

在普通函数中,return是用来终止函数的,只有在main函数中才是终止进程,而exit无论在哪里,一旦调用,整个进程就会终止。

3.进程等待

3.1 为什么要有进程等待?

在讲进程概念时我们提到,当子进程退出,父进程如果不管不顾,子进程残留资源(PCB)存放于内核中,就可能会造成僵尸进程。如果该资源不能得到释放,就会导致内存泄漏。僵尸进程是不能使用 kill -9 命令清除掉的。因为 kill 命令只是用来终止进程的, 而僵尸进程已经终止。

同时,父进程派给子进程的任务完成的如何,我们是需要知道的。例如,子进程运行完成,结果对还是不对, 或者是否正常退出。

因此,就需要父进程通过进程等待的方式,回收子进程的资源。

3.2 进程等待的方法

一个进程在终止时会关闭所有文件,释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。当这个进程的父进程调用 wait 或 waitpid 获取这些信息后,才会将这个进程彻底清除掉。

一个进程的退出状态可以在 Shell 中通过运行echo $?查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出 状态同时彻底清除掉这个进程。

3.2.1 wait函数

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
  • 返回值:成功返回被等待进程pid,失败返回-1。
  • status:是一个输出型参数,将wait函数内部计算的结果通过status返回给调用者,父进程从而获取子进程退出状态,如果不关心子进程的退出状态则可以将参数设置成为NULL。

这里提一下输入型参数和输出型参数的区别,输入型参数是调用者给函数传的参数,而输出型参数是是函数将内部计算结果返回给调用者,因此输出型参数往往用指针。

父进程调用 wait 函数可以回收子进程终止信息。该函数有三个功能:

  • 阻塞等待子进程退出
  • 回收子进程残留资源
  • 获取子进程结束状态(退出原因)。

当父进程调用wait得到传出参数status后,可以借助宏函数来进一步判断进程终止的具体原因:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出

WEXITSTATUS(status): 若WIFEXITED非零,说明子进程正常终止,提取子进程退出码。(查看进程的退出码(exit 的参数))

3.2.2 waitpid函数

作用同 wait,但waitpid可指定 pid 进程清理,可以通过非阻塞方式等待子进程退出。

pid_ t waitpid(pid_t pid, int *status, int options);

pid:

  • pid = -1,等待任一子进程退出,此时与wait等效
  • pid > 0, 回收指定 ID 的子进程,pid为指定进程的进程号。如果不存在该子进程,则立即出错返回

status:

  • 同wait

option:

  • 0:阻塞模式,即父进程会阻塞在waitpid处,等到子进程退出后继续。
  • WNOHANG: 非阻塞模式,若pid指定的子进程没有结束,则waitpid函数返回0,不予以等待。若正常结束,则返回该子进程的ID。一般情况下,非阻塞模式需要搭配循环使用。

注意:一次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环。

返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在

3.3.3 子进程的status

关于status的用法,我已经在wait函数处讲解,此处不再赘述。这里将从底层的角度剖析status的含义。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)。

我们以下一段代码为例,来展示一下非阻塞等待方式

#include <stdio.h> 
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
   pid_t pid;
    
    pid = fork();
    if(pid < 0){
       printf("%s fork error\\n",__FUNCTION__);
        return 1;
         
    }else if( pid == 0  ){ //child
     printf("child is run, pid is : %d\\n",getpid());
     sleep(5);
     exit(1);
    } else{
       int status = 0;
       pid_t ret = 0;
       do
       {
       ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
       if( ret == 0  ){
       printf("child is running\\n");
       }
       sleep(1); 
       }while(ret == 0);
          
       if( WIFEXITED(status) && ret == pid  ){
          printf("wait child 5s success, child return codeis:%d.\\n",WEXITSTATUS(status)); 
          }else{
          printf("wait child failed, return.\\n");
          return 1;
          }
    }
     return 0;
}

这段代码先创建子进程,让子进程等待5s再退出,父进程每1s检查一下,5s后子进程退出,ret将变成子进程的进程号,退出循环等待。最终的运行结果如下:

4.进程替换

4.1进程替换的原理

在讲进程替换原理前,我们需要先知道什么是进程替换。在讲fork函数时我们提到,fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),如果此时我们用一个新的程序替换掉子进程的地址空间、代码段和数据,子进程将会从新程序的启动例程开始执行,这就是进程替换。

进程替换并不是创建新的进程,因为替换前后该进程的PID并未改变。

4.2 环境变量

进程替换需要用到一种exec函数,在讲exec函数族之前,我们先介绍一下环境变量的概念。

4.2.1常见的环境变量

按照惯例,环境变量字符串都是name=value 这样的形式,大多数 name 由大写字母加下划线组成,一般把name 的部分叫做环境变量,value 的部分则是环境变量的值。

环境变量定义了进程的运行环境,具有全局属性,因此设置环境变量时要加export,一些比较重要的环境变量的含义如下:

PATH

可执行文件的搜索路径。ls 命令也是一个程序,执行它不需要提供完整的路径名/bin/ls, 然而通常我们执行当前目录下的程序 a.out 却需要提供完整的路径名./a.out,这是因为 PATH 环境变量的值里面包含了 ls 命令所在的目录/bin,却不包含 a.out 所在的目录。

PATH 环境变量的值可以包含多个目录,用:号隔开。在 Shell 中用 echo 命令可以查看这个环境变量的值: echo $PATH

SHELL

当前 Shell,它的值通常是/bin/bash。

TERM

当前终端类型

HOME

当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。

4.2.2与环境变量相关的函数

getenv函数

获取环境变量值: char *getenv(const char *name);

成功:返回环境变量的值;失败:NULL (name 不存在)

setenv 函数

设置环境变量的值 :int setenv(const char *name, const char *value, int overwrite);

成功:返回0;失败: 返回-1

参数 overwrite 取值:

1:覆盖原环境变量

0:不覆盖。(该参数常用于设置新环境变量,如:HELLO = “hello”)

unsetenv 函数

删除环境变量 name 的定义: int unsetenv(const char *name);

成功:0;失败:-1

注意事项:name 不存在仍返回 0(成功)。

4.2.3 环境变量的组织形式

environ 变量是一个char ** 类型,存储着系统的环境变量。每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\\0’结尾的环境字符串。

4.3 exec函数族

4.3.1 exec函数族的使用

知道了环境变量的概念后,再简要介绍一下命令行参数。当我们在某个目录下输入ls -als -l时,会有如下显示:

我们发现,同样的ls命令,由于后面所跟的字符串不同,显示了不同的结果。这里的“-a”,“-l”被称为参数。实际上,一个程序内可以通过加入参数,让相同的程序执行不同的功能。

接下来我们来介绍进程替换必不可少的函数族——exec函数族。

其实有六种以 exec 开头的函数,统称 exec 函数:

int execl(const char *path, const char *arg, ...); 

int execlp(const char *file, const char *arg, ...);

int execle(const char *path, const char *arg, ..., char *const envp[]);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]); 

int execve(const char *path, char *const argv[], char *const envp[]);

注意:这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。 如果调用出错则返回-1 所以exec函数只有出错的返回值而没有成功的返回值

这些函数如何使用,我们来看下面这段代码:

#include <unistd.h>
int main()
{
 char *const argv[] = {"ps", "-ef", NULL};//argv[0]始终是程序名
 char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
 //execl("/bin/ps", "ps", "-ef", NULL);
 
 // 带p的,可以使用环境变量PATH,无需写全路径
 //execlp("ps", "ps", "-ef", NULL);
 
 
 // 带e的,需要自己组装环境变量
 //execle("ps", "ps", "-ef", NULL, envp);
 
 //execv("/bin/ps", argv);
 
 // 带p的,可以使用环境变量PATH,无需写全路径
 //execvp("ps", argv);

 // 带e的,需要自己组装环境变量
 execve("/bin/ps", argv, envp);
 exit(0);
}

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve。

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  • l(list) : 表示参数采用列表,如果采用列表形式,const char *arg中的第一个参数必须是可执行程序本身,如上例中的 “ps”。
  • v(vector) : 参数用数组 ,v和l只能二选一
  • e(env) : 表示自己维护环境变量,有e参数中就需要有char *const envp[]
  • p(path) : 有p自动搜索环境变量PATH,第一个参数直接输入程序名即可,且有p一定没有e,因为有表示已经自动添加了环境变量,如果没有p则需要输入对应程序的路径

4.3.2 进程替换的应用

我们平时使用的shell读取命令和分析命令就是一个很典型的例子,如下图所示:

我们平时输入的如ls -a等命令实际上是一个个可执行程序。当shell读取一行命令时,shell会对命令进行解析,并且shell创建一个子进程,再通过调用execve,用可执行程序替换掉子进程,当程序执行完毕并且退出后,shell读取子进程的退出信息。这样,即便会出现程序崩溃的情况,也不会影响到shell本身。

以上就是关于进程控制的内容,主要分为四个方面——进程创建,进程终止,进程等待以及进程替换。有了以上的知识,我们已经可以实现一个很简易的shell,如何实现,请读者自行思考!

以上是关于Linux系统:进程控制的主要内容,如果未能解决你的问题,请参考以下文章

Linux进程和服务的控制

linu学习笔记--进程基础

Linux---进程及服务的控制

Linux 124课程 8管理本地的Linux用户和组控制服务和守护进程

Linux系统管理09——引导过程与服务控制

谁可以提供一些关于linux的进程控制的资料?尽快,非常感谢。