f 性能优化-【Linux】 Linux 进程控制 - Powered by qidao123.com技术社区

【Linux】 Linux 进程控制

打印 上一主题 下一主题

主题 1784|帖子 1784|积分 5352

参考博客:https://blog.csdn.net/sjsjnsjnn/article/details/125581083
一、进程创建

1.1 fork()函数



  • 在linux中fork函数黑白常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
  • 进程调用fork,当控制转移到内核中的fork代码后,内核做:

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

1.2 fork函数的返回值

返回值:

  • 给父进程返回子进程的PID;
  • 给子进程返回0;
  • 子进程创建失败会返回-1;
  1. void test1(){
  2.     int ret = fork();
  3.     std::cout << "pid = " << getpid() << ", fork()  = " << ret << std::endl;
  4. }
复制代码


  • 参照运行结果可以看出,给父进程返回的值为子进程的pid,而子进程返回的是0

创建子进程之后,也就是调用fork()函数之后,新创建的进程才开始实行之后的操作,如下图所示

fork之前父进程独立实行,fork之后,父子两个实行流分别实行。留意,fork之后,谁先实行完全由调度器决定。
1.3 写时拷贝



  • 通常,父子代码共享,父子再不写入时,数据也是共享的,当恣意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:



  • 在修改内容之前,父子进程的数据和代码都是共享的
  • 当恣意一方试图写入时,操作系统会辨认到缺页停止
  • 所谓的缺页停止:是指计算机在实行程序的过程中,当出现异常情况或特殊请求时,计算机制止现行程序的运行,转向对这些异常情况或特殊请求的处置惩罚,处置惩罚竣事后再返回现行程序的间断处,继续实行原程序。
  • 那么,操作系统重新分配一块空间,将旧空间的数据拷贝下来,此时操作系统也会重新映射页表。
1.4 fork函数通例用法



  • 一个父进程希望复制本身,使父子进程同时实行差别的代码段。比方,父进程等候客户端请求,天生子进程来处置惩罚请求。
  • 一个进程要实行一个差别的程序。比方子进程从fork返回后,调用exec类函数。
1.5 fork函数调用失败的原因



  • 系统中有太多的进程,导致内存严重不敷,无法加载数据
  • 现实用户的进程数凌驾了限制
二、进程终止

2.1 进程的退进场景以及退出码

进程一旦退出,就会存在以下三种情况:

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


  • 这三种情况,作为用户怎样才气知道某个进程是以什么样的情势退出的呢?那么就有了退出码的概念。
  • Linux 系统中,程序可以在实行终止后通报值给其父进程,这个值被称为退出码。
  • 用户就可以通过相应的退出码,对进程退出状态做以判定
  • 比方,我们的main函数,每次都会写上 return 0; 其实这就是进程的退出码。
我们可以通过 echo $?
来获取最近一次进程退出时的退出码。

  1. echo $?
复制代码


  • 上次进程退出是正常退出,因此结果为0
  • 对于每个指令,对应的都是一个个进程,我们输错指令也会有错误的进程返回值,比如下面输入lsss,返回值为127

2.2 查询返回值的寄义

可以通过strerror()函数来检察对应返回值的寄义
  1. void test2(){
  2.     for(int i = 0;i<140;++i){
  3.         std::cout << "error[" << i << "] = " << strerror(i) << std::endl;
  4.     }
  5. }
复制代码
现实上只有133以内的才是有寄义的,打印的结果如下:


三、进程常见的退出方法

3.1 return退出



  • 刚刚我们已经介绍过main函数是通过return退出进程,须要留意以其他函数(非main函数)return进行区分,非main函数的return是函数返回,而main函数的return是进程退出。
3.2 exit( )退出



  • exit可以在程序的任何位置退出,exit退出会刷新缓冲区,和return一样
  1. void test3(){
  2.     std::cout << "test3 exit(100)" <<  std::endl;
  3.     sleep(1);
  4.     exit(100);
  5. }
复制代码


  • 检察进程的返回值,eixt(100)退出的返回值就为100

3.3 _exit退出



  • 除了上面两种方法来退出进程,我们还可以使用_exit函数来使进程退出。
  • _exit也是可以在代码中的任何位置终止进程,但是_exit函数终止进程时,是逼迫终止,不会进行进程的后续收尾工作,如:刷新缓冲区
exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

  • 实行用户通过 atexit或on_exit定义的整理函数。
  • 关闭所有打开的流,所有的缓存数据均被写入
  • 调用_exit
3.4 return、exit 和 _exit 的区别


  • _exit()实行后会立即返回给内核,而exit()要先实行一些清除操作,然后将控制权交给内核。
  • 调用_exit()函数时,其会关闭进程所有的文件描述符,整理内存,以及其他一些内核整理函数,但不会刷新流(stdin 、stdout、stderr)。exit()函数是在_exit()函数上的一个封装,它会调用_exit,并在调用之前先刷新流。
  • return是一种更常见的退出进程方法。实行return(num)等同于实行exit(num),由于调用main的运行时函数会将main的返回值当做 exit的参数。

3.5 异常退出



  • 以上是正常退出的情况,和进程的退出码有关;
  • 对于进程的异常退出,就是程序实行了一半后由于地点访问错误、主动终止进程(比如ctrl+c大概kill,也有对应的错误码,这个错误码现实上是包含了进程的终止信号,下面会讲解
四、进程等候

4.1 进程等候的须要性


  • 子进程退出,父进程如果不获取到子进程的退出信息,就可能造成 僵尸进程 的题目,进而造成内存走漏。
  • 进程一旦变成僵尸状态,所谓的 kill -9 也无能为力,由于谁也没有办法杀死一个已经死去的进程。
  • 父进程派给子进程的任务完成的如何,我们须要知道。如,子进程运行完成,结果对还是不对,大概是否正常退出。
  • 父进程通过进程等候的方式,回收子进程资源,获取子进程退出信息。
4.2 进程等候方法

4.2.1 wait方法

函数原型以及所需头文件
  1. #include <sys/types.h>
  2. #include <sys/wait.h>
  3. pid_t wait(int *status);
复制代码


  • 返回值:等候成功则返回等候进程的PID,等候失败,返回-1;
  • 参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
  • 下面的代码演示了创建一个子进程,子进程实行对应的任务,然后自动退出,父进程等候子进程竣事,而且回收子进程的内存
  1. void test4(){
  2.     pid_t id = fork();
  3.     if(id == 0){
  4.         for(int i =0 ;i< 5 ; ++i){
  5.             std::cout << "child id = " << getpid() << ", i = " << i << std::endl;
  6.             sleep(1);
  7.         }
  8.         exit(0);
  9.     }
  10.     else{
  11.         sleep(10);
  12.         std::cout << "father wait begin..." << std::endl;
  13.         pid_t cur = wait(NULL);
  14.         if(cur > 0){
  15.             std::cout << "father wait: "<< cur << " sucess" << std::endl;
  16.         }
  17.         else{
  18.             std::cout << "father wait failed!" << std::endl;
  19.         }
  20.         sleep(10);
  21.     }
  22. }
复制代码


  • 使用下面的shell脚原来监听对应的进程状态
  1. while :; do ps axj | head -1 && ps axj | grep mytest | grep -v grep; sleep 1; echo "**********************"; done
复制代码


  • 可以发现,在运行竣事后,子进程退出,然后被父进程回收了,最后父进程也退出了

运行结果

4.2.2 waitpid方法

函数原型以及所需头文件
  1. #include <sys/types.h>
  2. #include <sys/wait.h>
  3. pid_t waitpid(pid_t pid, int *status, int options);
复制代码
返回值:


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


  • pid:
    Pid=-1,等候任一个子进程。与wait等效。
    Pid>0.等候其进程ID与pid相称的子进程。
  • status:
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(检察进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(检察进程的退出码)
  • options:
    WNOHANG: 若pid指定的子进程没有竣事,则waitpid()函数返回0,不予以等候。若正常竣事,则返回该子进程的PID。
下面的代码为等候恣意一个子进程后,回收子进程,这里options指定为0,将会阻塞在此
  1. void test5()
  2. {
  3.     pid_t id = fork();
  4.     if (id == 0)
  5.     {
  6.         for (int i = 0; i < 5; ++i)
  7.         {
  8.             std::cout << "child id = " << getpid() << ", i = " << i << std::endl;
  9.             sleep(1);
  10.         }
  11.         exit(100);
  12.     }
  13.     sleep(10);
  14.     std::cout << "father wait begin..." << std::endl;
  15.     // pid_t cur = wait(NULL);
  16.     int status = 0;
  17.     pid_t cur = waitpid(-1, &status, 0); // 等待任意一个子进程
  18.     if (cur > 0)
  19.     {
  20.         std::cout << "father wait: " << cur << " sucess, WIFEXITED(status) = " << WIFEXITED(status)
  21.          <<",WEXITSTATUS(status) = "<< WEXITSTATUS(status) <<  std::endl;
  22.     }
  23.     else
  24.     {
  25.         std::cout << "father wait failed!" << std::endl;
  26.     }
  27.     sleep(10);
  28. }
复制代码


  • 运行结果如下,通过WIFEXITED宏和WEXITSTATUS可以检察子进程是否正常退出以及退出码是多少

4.2.3 获取子进程status

4.2.3.1 什么是status

   int status:它是一种输出型的参数*
所谓获取子进程的status,就是获取子进程退出时的退出信息;
起首,在子进程中分别用exit(0)和exit(10)来停止子进程,父进程获取status值,判定进程的退出状态。
  4.2.3.2 status的构成



  • status是由32个比特位构成的一个整数,目前阶段我们只使用低16个位来表现进程退出的结果
  • 如下图所示,就是status低16位的表现图;
  1. status exit_code = (status >> 8) & 0xFF; //退出码
  2. status exit_code = status7 & 0x7F;       //退出信号
复制代码



  • 进程正常退出有两种,与退出码有关,异常退出与信号有关
  • 以是这里我们就须要获取到两组信息:退出码与信号
  • 如果没有收到信号,就表明我们所实行的代码是正常跑完的,然后在判定进程的退出码,毕竟是何原因使进程竣事的
  • 反之则是异常退出,也就不须要关心退出码了
  1. void test6()
  2. {
  3.     pid_t id = fork();
  4.     if (id == 0)
  5.     {
  6.         for (int i = 0; i < 5; ++i)
  7.         {
  8.             std::cout << "child id = " << getpid() << ", i = " << i << std::endl;
  9.             sleep(1);
  10.         }
  11.         exit(100);
  12.     }
  13.     sleep(10);
  14.     std::cout << "father wait begin..." << std::endl;
  15.     // pid_t cur = wait(NULL);
  16.     int status = 0;
  17.     pid_t cur = waitpid(-1, &status, 0); // 等待任意一个子进程
  18.     if (cur > 0)
  19.     {
  20.         std::cout << "father wait: " << cur << " sucess, status = " << status << std::endl;
  21.         std::cout << "exit_code = " << ((status >> 8)& 0xff) << ", exit_signal = " <<(status & 0x7f) << std::endl;
  22.     }
  23.     else
  24.     {
  25.         std::cout << "father wait failed!" << std::endl;
  26.     }
  27.     sleep(10);
  28. }
复制代码


  • 正常退出,这里退出码为100,终止信号可以忽略



  • 在进程运行时使用kill -9下令终止进程,可以发现终止信号为9



  • 查询shell指令,发现9对应的是SIGKILL
  1. kill -l
复制代码



  • 这里可以通过WIFEXITED宏和WEXITSTATUS宏检察是否是正常退出,以及正常退出的返回值
  1. pid_t cur = waitpid(-1, &status, 0); // 等待任意一个子进程
  2. if (cur > 0)
  3. {
  4.         std::cout << "father wait: " << cur << " sucess, WIFEXITED(status) = " << WIFEXITED(status)
  5.          <<",WEXITSTATUS(status) = "<< WEXITSTATUS(status) <<  std::endl;
  6. }
  7. else
  8. {
  9.         std::cout << "father wait failed!" << std::endl;
  10. }
复制代码
4.2.3.3 阻塞等候与非阻塞等候



  • 这里我们所讲的阻塞等候和非阻塞等候,其实就是waitpid函数的第三个参数,我们之前并未提及,直接给的是0,这种是默认举动,阻塞等候;阻塞等候:父进程一直在等候子进程,什么事都不干,直到子进程正常退出。
  • 如果设置为WNOHANG,表现的黑白阻塞等候方式。非阻塞等候:父进程的PCB由运行队列转变为等候队列,直达子进程竣事,操作系统获取到子进程退出的信号时,再将父进程从等候队列中调度到运行队列,由父进程去获取子进程的退出码以及退出信号。
通过判定返回值来检察是否子进程已经竣事了,如果没有竣事就继续干父进程本身的任务,否则就回收子进程
  1. void test7()
  2. {
  3.     pid_t id = fork();
  4.     if (id == 0)
  5.     {
  6.         for (int i = 0; i < 10; ++i)
  7.         {
  8.             std::cout << "child id = " << getpid() << ", i = " << i << std::endl;
  9.             sleep(1);
  10.         }
  11.         exit(100);
  12.     }
  13.     std::cout << "father wait begin..." << std::endl;
  14.     while (true)
  15.     {
  16.         int status = 0;
  17.         pid_t cur = waitpid(-1, &status,WNOHANG); // WNOHANG = 1,非阻塞
  18.         if (cur > 0)
  19.         {
  20.             std::cout << "father wait: " << cur << " sucess, status = " << status << std::endl;
  21.             std::cout << "exit_code = " << ((status >> 8) & 0xff) << ", exit_signal = " << (status & 0x7f) << std::endl;
  22.             break;
  23.         }
  24.         else if(cur == 0)
  25.         {
  26.             std::cout << "do father process things" << std::endl;
  27.             sleep(1);
  28.         }
  29.         else{
  30.             std::cout << "father wait failed!" << std::endl;
  31.             break;
  32.         }
  33.     }
  34. }
复制代码
运行结果如下

五、进程程序更换

5.1 更换原理



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



  • 从上图可以看出,进程程序更换前后,进程本身并没有发生任何变化,只是所实行的代码发什么改变。
  • 如果子进程进行程序更换,不会影响父进程的代码和数据吗?起首进程是具有独立性的,虽然子进程共享父进程的代码和数据,但是由于进行了函数更换,发生了代码和数据的修改,此时就会进行写时拷贝。所有子进程进行程序更换时,并不会影响父进程的代码和数据。
5.2 更换函数

有六种以exec开头的函数,统称exec函数: 他们所需的头文件均为 #include <unistd.h>
execl函数
  1. int execl(const char *path, const char *arg, ...);
  2. // path --- 可执行程序的路径
  3. // arg --- 可变参数列表,表示你要如何执行这个程序,并以NULL结尾
  4. // 例如:
  5. execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
复制代码
execlp函数
  1. int execlp(const char *file, const char *arg, ...);
  2. // file --- 可执行程序的名字
  3. // arg --- 可变参数列表,表示你要如何执行这个程序,并以NULL结尾
  4. // 例如:
  5. execlp("ls", "ls", "-a", "-l", NULL);
复制代码
execle函数
  1. int execle(const char *path, const char *arg, ..., char * const envp[]);
  2. // path --- 可执行程序的路径
  3. // arg ---  可变参数列表,表示你要如何执行这个程序,并以NULL结尾
  4. // envp --- 自己维护的环境变量
  5. // 例如:
  6. char* envp[] = { "Myval=12345", NULL };
  7. execle("./myexe", "myexe", NULL, Myval);
复制代码
execv函数
  1. int execv(const char *path, char *const argv[]);
  2. // path --- 你要执行程序的路径
  3. // argv --- 指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
  4. // 例如:
  5. char* argv[] = { "ls", "-a", "-l", NULL };
  6. execv("/usr/bin/ls", argv);
复制代码
execvp函数
  1. int execvp(const char *file, char *const argv[]);
  2. // file --- 你要执行程序的名字
  3. // argv --- 指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
  4. // 例如:
  5. char* argv[] = { "ls", "-a", "-l", NULL };
  6. execvp("ls", argv);
复制代码
execve函数
  1. int execvpe(const char *file, char *const argv[], char *const envp[]);
  2. // file --- 你要执行程序的路径
  3. // argv --- 指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾
  4. // envp --- 自己维护的环境变量
  5. //例如:
  6. char* argv[] = { "mycmd", NULL };
  7. char* envp[] = { "Myval=12345", NULL };
  8. execve("./myexe", argv, envp);
复制代码
函数解释


  • 这些函数如果调用成功则加载新的程序从启动代码开始实行,不再返回。
  • 如果调用堕落则返回-1
  • 以是exec函数只有堕落的返回值而没有成功的返回值。也就是说,exec系列函数只要返回了,就意味着调用失败。
函数名参数格式是否带路径是否使用当前情况变量execl列表不是是execlp列表是是execle列表不是不是,须本身装情况变量execv数组不是是execvp数组是是execve数组不是不是,须本身装情况变量
  1. void test8(){
  2.     char *argv[] = {"ls","-a","-l",NULL};
  3.    
  4.     execl("/usr/bin/ls","ls","-a","-l",NULL); //可变参,NULL结尾
  5.     execv("/usr/bin/ls",argv); //字符串数组形式
  6.     execlp("ls","ls","-a","-l"); //文件名+可变参
  7.     execvp("ls",argv); //字符串数组形式
  8.     //可执行文件路径
  9.     char* argv_[] = {"./process-test","-test2",NULL};
  10.     char* env_[] = {NULL};
  11.     execvpe("./process-test",argv_,env_);
  12. }
复制代码

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


  • l(list) : 表现参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索情况变量PATH
  • e(env) : 表现本身维护情况变量
事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,以是execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数现实上是对系统调用execve进行了封装,以满意差别用户的差别调用场景的。
下图为exec系列函数族之间的关系:

更多资料:https://github.com/0voice

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

梦见你的名字

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表