相关推荐recommended
Linux下进程控制
作者:mmseoamin日期:2024-02-06

文章目录

  • 创建进程
    • fork创建进程
      • fork返回值
      • 写诗拷贝
      • fork常规用法
      • fork失败的原因
      • 进程终止
        • 进程正常终止
        • 查看进程退出码
        • _exit函数
        • exit函数
        • exit 和 _exit 的区别
        • return退出
        • 进程等待
          • 进程等待的方式
            • wait方法(系统调用)
            • waitpid方法(系统调用)
            • WEXITSTATUS 和 WIFEXITED
            • 阻塞等待和非阻塞等待
            • 进程程序替换
              • 替换函数
                • execl函数
                • execlp函数
                • execle函数
                • execv函数
                • execvp函数
                • execvpe函数
                • exec系列函数
                • exec家族关系

                  创建进程

                  fork创建进程

                  fork函数

                  Linux下进程控制,image.png,第1张

                  返回值:子进程中返回0,父进程返回子进程id,出错返回-1。

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

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

                    Linux下进程控制,image.png,第2张

                    #include 
                    #include 
                    #include 
                    int main()
                    {
                        printf("before fork pid is :%d\n",getpid());
                        pid_t id = fork();
                        if(id < 0){
                            perror("fork");
                        }
                        printf("after fork pid id :%d\n",getpid());
                        
                        printf("fork return value is :%d\n",id);
                        return 0;
                    }
                    

                    上面程序运行结果

                    Linux下进程控制,image.png,第3张

                    可以看出,12行和14行中的打印信息被执行了两次,而且两次的值也不一样。

                    Linux下进程控制,image.png,第4张

                    因为fork之前父进程单独执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器 决定。

                    fork返回值

                    • 子进程返回0
                    • 父进程返回的是子进程的id

                      写诗拷贝

                      fork之后,父子进程共享代码,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。

                      Linux下进程控制,image.png,第5张

                      Linux下进程控制,image.png,第6张

                      fork常规用法

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

                        fork失败的原因

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

                          进程终止

                          进程退出的场景:

                          • 代码跑完,结果正确。
                          • 代码跑完,结果不正确
                          • 代码没跑完,进程出异常

                            进程正常终止

                            • main返回
                            • 调用_exit
                            • 使用exit

                              再平时写C/C++程序时,一般都i会这样写

                              #include 
                              int main()
                              {
                                  //...
                                  return 0;
                              }
                              

                              都会在main函数最后一行写一句return 0;

                              return 0;就是进程的退出码,一般0表示成功,非0 表示失败。

                              一但程序执行失败,需要知道原因,用不同的数字来表示不同的失败原因。

                              查看进程退出码

                              使用echo $?可以查看最近一次进程的退出码

                              Linux下进程控制,image.png,第7张

                              比如系统中没有lll命令,执行,然后查看进程退出码,可以看到退出码为127

                              Linux下进程控制,image.png,第8张

                              在C标准库函数中,有一个strerror函数,用于将错误码(通常是由系统调用或库函数返回的错误码)转换为对应的错误消息字符串。

                              #include 
                              #include 
                              int main()
                              {
                                  for (size_t i = 0; i < 255; i++)
                                  {
                                      printf("error code %d:%s\n",i,strerror(i));
                                  }
                                  return 0;
                              }
                              

                              Linux下错误码一共有133个。

                              Linux下进程控制,image.png,第9张

                              _exit函数

                              Linux下进程控制,image.png,第10张

                              参数:status 定义了进程的终止状态,父进程通过wait来获取该值

                              _exit在程序的任意位置都可以终止进程

                              #include 
                              #include 
                              void test()
                              {
                                  printf("Hello World\n");
                                  _exit(1);
                              }
                              int main()
                              {
                                  test();
                                  return 0;
                              }
                              

                              运行结果:

                              Linux下进程控制,image.png,第11张

                              main函数中的return 0;还没有执行到进程就终止了。

                              exit函数

                              exit和_exit 的区别,exit是c语言的进程终止的函数,而_exit是Linux系统调用接口的函数,c语言在实现exit函数时会封装_exit。

                              Linux下进程控制,image.png,第12张

                              #include 
                              #include 
                              void test()
                              {
                                  printf("Hello World\n");
                                  exit(1);
                              }
                              int main()
                              {
                                  test();
                                  return 0;
                              }
                              

                              进程退出码和上面一样,都是1,main函数中的return 0;不会执行,进程就终止了。

                              exit 和 _exit 的区别

                              • eixt会刷新缓冲区 更推荐
                              • _eixt 不会刷新缓冲区。
                              • exit封装了_exit;

                                Linux下进程控制,image.png,第13张

                                Linux下进程控制,image.png,第14张

                                同样的代码,分别使用eixt 和 _exit终止进程,exit会刷新缓冲区。(printf语句没有加\n),而_exit则不会刷新缓冲区。

                                return退出

                                • return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返 回值当做 exit的参数。
                                • 只有在main函数中return才能起到进程终止的作用,在其他函数中return不会终止进程,仅仅只是函数调用结束。

                                  进程等待

                                  进程等待的必要性:

                                  • 子进程退出,父进程不等待,子进程可能变僵尸,从而造成内存泄漏
                                  • 一个进程一旦变为僵尸,谁都无能为力,kill也杀不掉,因为,无法杀掉一个已经死掉的进程。
                                  • 父进程创建子进程,子进程的任务完成的情况,我们需要知道。子进程的运行结果是否正确,是否正常退出(比如ls命令 是bash的子进程,ls的执行情况是需要我们知道的)
                                  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。(父进程必须要做的)

                                    进程等待的方式

                                    wait方法(系统调用)

                                    man 2 wait 认识wait

                                    Linux下进程控制,image.png,第15张

                                    返回值:

                                    等待成功,返回子进程的pid

                                    等待失败,返回-1。

                                    函数参数:

                                    输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

                                    wait:它等待任意子进程退出,而不需要指定特定的子进程ID。如果没有子进程退出,wait 会阻塞当前进程直到有子进程退出。(阻塞等待简单理解就是父进程啥也不干,就只等子进程退出进行回收)

                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    #include 
                                    int main()
                                    {
                                        pid_t id = fork();
                                       if(id < 0 )
                                       {
                                        perror("fork\n");
                                       }
                                       else if(id == 0){
                                        //子进程
                                        printf("i am child process.pid = %d\n",getpid());
                                        //子进程退出 退出码为1 
                                        exit(1);
                                       }
                                       else{
                                         //父进程
                                        printf("i am Parent process waiting...\n");
                                        sleep(3);//父进程等待三秒后回收子进程
                                        int status = 0;
                                        pid_t waitid = wait(&status);//不想获取子进程退出码可以设置为NULL
                                        //等待失败处理
                                        if(waitid == -1)
                                        {
                                            perror("wait fail\n");
                                            exit(-1);
                                        }
                                        printf("wait sucess, child excit code = %d,waitid = %d\n",status,waitid);
                                       }
                                        return 0;
                                    }
                                    

                                    运行结果:

                                    Linux下进程控制,PixPin_2024-01-04_21-04-23.gif,第16张

                                    Linux下进程控制,image.png,第17张

                                    这里父进程获取到的子进程退出码并不是1,而是256

                                    这是因为status占4个字节,32个比特位。前十六位不用关心,后是6位前8位表示进程正常的退出码,最后七位表示进程异常收到的信号,退出码和信号中间的一位是core dump标志位,和信号有关,这里不用深究。

                                    Linux下进程控制,image.png,第18张

                                    exit(1) 则是进程正常退出,也没有收到异常信号,status则是

                                    Linux下进程控制,image.png,第19张

                                    这就是256的原因。

                                    如果想直接拿到进程的退出码,而不是status,(status>>8)& 0xff即可

                                    上面代码31行修改为

                                     printf("wait sucess, child excit code = %d,waitid = %d\n",(status>>8)&0xFF,waitid);
                                    

                                    运行结果:

                                    Linux下进程控制,image.png,第20张

                                    waitpid方法(系统调用)

                                    等待指定进程或者任意子进程,相比wait更灵活

                                    man 2 waitpid 认识waitpid

                                    Linux下进程控制,image.png,第21张

                                    参数:

                                    • pdi:等待子进程的id,若设置为-1,则和wait等效
                                    • status:输出型参数,获取子进程的退出状态,不关心子进程退出状态设置为NULL
                                    • options:设置等待方式,阻塞等待或者非阻塞等待

                                      返回值:

                                      • 等待成功返回被等待进程的pid。
                                      • 等待方式设置为非阻塞等待(WNOHANG),而调用中waitpid发现没有已退出的子进程可收集,则返回0;

                                        阻塞等待示例:

                                        #include 
                                        #include 
                                        #include 
                                        #include 
                                        #include 
                                        int main()
                                        {
                                            //waitpid阻塞等待
                                            pid_t id = fork();
                                            if(id < 0 ){
                                                perror("fork\n");
                                                exit(-1);
                                            }
                                            else if(id == 0){
                                                //子进程
                                                printf("i am child process. pid = %d\n",getpid());
                                                sleep(3);
                                                exit(1);
                                            }
                                            else {
                                                //父进程
                                               printf("parent process waiting ...\n");
                                               int status = 0 ;
                                               pid_t ret = waitpid(-1,&status,0);//阻塞等待任意子进程
                                              
                                               if(WIFEXITED(status) && ret == id){//等待成功
                                                printf("wait sucess,child return code = %d,ret = %d\n",WIFEXITED(status),ret);
                                               }
                                               else{//等待失败
                                                printf("wait fail\n");
                                               }
                                            }
                                            return 0;
                                        }
                                        

                                        运行结果:

                                        Linux下进程控制,image.png,第22张

                                        等待成功,子进程退出码为1,waitpid返回值为子进程的pid

                                        WEXITSTATUS 和 WIFEXITED

                                        WIFEXITED 和 WEXITSTATUS 是在 头文件中定义的两个宏,用于处理子进程的退出状态信息。

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

                                          非阻塞等待示例:

                                          #include 
                                          #include 
                                          #include 
                                          #include 
                                          #include 
                                          int main()
                                          {
                                              //waitpid非阻塞等待
                                              pid_t id = fork();
                                              if(id < 0 ){//失败处理
                                                  perror("fork\n");
                                                  exit(-1);
                                              }
                                              else if(id == 0){//子进程
                                                  printf("i am child process. pid = %d\n",getpid());
                                                  sleep(3);
                                                  exit(1);
                                              }
                                              else {//父进程
                                                 printf("parent process waiting ...\n");
                                                 int status = 0;
                                                 pid_t ret  = 0;
                                                 do
                                                 {
                                                      ret = waitpid(-1,&status,WNOHANG);//非阻塞等待任意子进程
                                                      if(0 == ret){//子进程还没退出,父进程非阻塞等待可以做其他事情
                                                          printf("child process is running,parent do other things\n");
                                                          sleep(1);
                                                      }
                                                 }while(0 == ret);
                                                 //等待成功
                                                 if(WIFEXITED(status) && ret == id){
                                                 //打印子进程退出码和waitpid返回值
                                                  printf("wait sucess,child return code = %d,ret = %d\n",WEXITSTATUS(status),ret);
                                                 }
                                                 //等待失败
                                                 else{
                                                  printf("wait fail\n");
                                                 }
                                              }
                                              return 0;
                                          }
                                          

                                          运行结果:

                                          Linux下进程控制,PixPin_2024-01-08_21-21-19.gif,第23张

                                          Linux下进程控制,image.png,第24张

                                          父进程非阻塞等待子进程时还可以做其他的事情, wiatpid返回值时子进程的id

                                          阻塞等待和非阻塞等待

                                          阻塞等待:

                                          阻塞等待会导致进程无法执行其他任务,直到等待的事件发生。

                                          waitpid函数设置为阻塞等待,只需将options参数设置为0

                                          • 阻塞等待的优点:

                                            • 实现简单,易于理解。
                                            • 不需要额外的代码来检查事件状态。
                                            • 阻塞等待的缺点:

                                              • 整个进程或线程被挂起,无法执行其他任务。
                                              • 可能导致资源浪费,因为进程被阻塞时,它可能无法充分利用系统资源

                                                非阻塞等待:

                                                waitpid函数设置为阻塞等待,需要将options参数设置为WNOHANG。(一个宏)

                                                • 非阻塞等待的优点:

                                                  • 可以充分利用系统资源,因为在等待事件的同时可以执行其他任务。
                                                  • 更灵活,适用于需要同时处理多个任务的情况。
                                                  • 非阻塞等待的缺点:

                                                    • 实现较为复杂,需要额外的代码来轮询或处理事件通知。
                                                    • 可能会增加系统负载,因为需要周期性地检查事件状态

                                                      进程程序替换

                                                      进程程序替换是指一个正在运行的进程将自己的地址空间、代码、数据和堆栈等信息替换为另一个程序的内容。

                                                      • 用fork创建的子进程和父进程执行的是相同的程序,但有可能执行不同的代码分支。一般fork创建的子进程需要调用exec函数来执行另一个程序。
                                                      • 当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
                                                      • 调用exec并不会创建新的子进程,所以调用exec前后该进程的id并未改变。
                                                      • 程序替换是通过特定的接口,加载到磁盘上的一个程序,加载到调用进程的地址空间中

                                                        Linux下进程控制,image.png,第25张

                                                        替换函数

                                                        有六种以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 execvpe(const char *file, char *const argv[], char *const envp[]);
                                                        
                                                        execl函数
                                                        int execl(const char *path, const char *arg, ...);
                                                        
                                                        • 参数1:path 是要执行的程序的路径(需要指定路径)。
                                                        • 参数2:是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

                                                          示例:

                                                          #include 
                                                          #include 
                                                          #include 
                                                          #include 
                                                          #include 
                                                          int main()
                                                          {
                                                              //进程程序替换
                                                              pid_t id = fork();
                                                              if(id < 0){
                                                                  perror("fork fail\n");
                                                              }
                                                              else if(id == 0){
                                                                  //子进程
                                                                  printf("i am child process, pid =  %d",getpid());
                                                                  //子进程执行ls -a 命令
                                                                  execl("/usr/bin/ls","ls","-a",NULL);
                                                                  printf("exec end\n");
                                                                  exit(1);
                                                              }
                                                              else{
                                                                  //父进程
                                                                  int status = 0;
                                                                  pid_t  ret = waitpid(id,&status,0);//阻塞等待指定子进程
                                                                  if(WIFEXITED(status) && ret == id)//等待成功
                                                                  {
                                                                      printf("wait sucess,child return code = %d,ret = %d, id = %d\n",WEXITSTATUS(status),ret,id);
                                                                  }
                                                              }
                                                              return 0;
                                                          }
                                                          

                                                          运行结果:

                                                          Linux下进程控制,image.png,第26张

                                                          可以发现 当进程程序替换完成后,exec后面的代码将不再执行。

                                                          一旦exec替换失败,才会只执行后面的代码

                                                          比如将上面17行要替换的进程改为一个不存在的,

                                                          execl("/usr/bin/lsl","ls","-l",NULL);
                                                          

                                                          执行结果:子进程后面的代码被执行了。

                                                          Linux下进程控制,image.png,第27张

                                                          execlp函数
                                                          int execlp(const char *file, const char *arg, ...);
                                                          
                                                          • 参数1:需要执行的程序名称。只需要指定程序名称,不需要指定路径
                                                          • 参数2:是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

                                                            示例:子进程执行ls命令

                                                            execlp("ls","ls","-l",NULL);
                                                            

                                                            Linux下进程控制,image.png,第28张

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

                                                            参数1:需要执行程序的路径,

                                                            参数2:是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

                                                            参数3:envp:是一个以 NULL 结尾的字符串数组,用于设置新程序的环境变量。

                                                            // 设置新程序的环境变量
                                                            char *envp[] = {"MY_VARIABLE=value", NULL};
                                                            // 使用 execle 函数替换当前进程的程序
                                                            execle("/bin/echo", "echo", "Hello, execle!", (char *)NULL, envp);
                                                            

                                                            运行结果:

                                                            Linux下进程控制,image.png,第29张

                                                            execv函数
                                                            int execv(const char *path, char *const argv[]);
                                                            
                                                            • 参数1:path 是要执行的程序的路径。
                                                            • 参数2:argv 是一个以 NULL 结尾的指针数组,其中包含新程序的名称和参数。
                                                              char *const argv[] = {"ls","-l",NULL};
                                                              execv("/usr/bin/ls",argv);
                                                              

                                                              运行结果:

                                                              Linux下进程控制,image.png,第30张

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

                                                              不用指定路径即可。

                                                              execvpe函数
                                                              int execvpe(const char *file, char *const argv[], char *const envp[]);
                                                              

                                                              第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量。

                                                              exec系列函数