Linux:进程控制(创建/终止/等待/获取退出信息/多进程)

上篇文章:Linux:进程虚拟地址空间|虚拟内存管理


目录

1.进程创建

1.1fork函数

1.2写时拷贝

1.3fork常规用法

1.4fork调用失败的原因

1.5实际运用

2.进程终止

2.1退出场景

退出信号举例:

2.2常见退出方法

2.2.1退出码

2.2.2strerror

2.2.3错误码和退出码区别

2.2.4进程正常退出

3.进程等待

3.1进程等待必要性

3.2进程等待的方法

3.2.1wait方法

3.2.2waitpid方法

3.2.3获取子进程status

3.2.4如何获取到子进程退出信息

3.2.5waitpid中的options

4.编写一个一次等待多个子进程的代码


1.进程创建

进程 = 内核数据结构(task_strct + mm_struct + vm_area_struct ...)(侧重进程管理)+ 代码和数据(侧重进程执行),其本质就是系统内多了一个进程。并且进程具有独立性,表现为内核结构独立,代码和数据独立。

1.1fork函数

在 linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

复制代码
#include <unistd.h>
pid_t fork(void);
返回值:⼦进程中返回0,⽗进程返回⼦进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核会分配新的内存块和内核数据结构给子进程;将父进程部分数据结构内容拷贝到子进程;添加子进程到系统进程列表当中;fork返回,开始调度器调度。

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方,但每个进程都将开始它们各自的行为:

复制代码
int main( void )
{
     pid_t pid;
     
     printf("Before: pid is %d\n", getpid());
     if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
     printf("After:pid is %d, fork return %d\n", getpid(), pid);
         sleep(1);
     return 0;
}
 
运⾏结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0

上述代码中,进程43677没有打印before,原因是:

所以fork之前,父进程独立运行,fork之后,父子两个执行流分别执行,而fork之后谁先执行由调度器决定。

fork函数返回值,子进程返回0,父进程返回的是子进程的pid。

1.2写时拷贝

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

因为有写时拷贝的技术存在,所以父子进程得以彻底分离,完成了进程独立性的技术保证。写时拷贝是一种延时申请技术,可以提高整机内存的使用率。

1.3fork常规用法

1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如:父进程等待客户端请求,生成子进程来处理请求。

2.一个进程要执行一个不同的程序,例如子进程从fork返回后,调用exec函数。

1.4fork调用失败的原因

1.系统中有太多的进程

2.实际用户的进程数超过了限制

1.5实际运用

使用fork + 写时拷贝技术,进行安全备份。一般用在内存数据库中,进行数据持久化,也就是保存到磁盘当中。

复制代码
    1 #include <stdio.h>
    2 #include <unistd.h>
    3 #include <time.h>
E>  4 #include <stdlib.h>
    5 
    6 #define NUM 10
    7 
    8 int data[NUM] = {0};
    9 
   10 void Backup()
   11 {
   12     // father
   13     pid_t id = fork();
   14     if(id == 0)
   15     {
   16         // child
   17         int i = 0;
   18         printf("Backip: ");
   19         for(i = 0; i < NUM; i++)
   20         {
   21             printf("%d", data[i]);
   22         }
   23         printf("\n");
   24         sleep(10);
E> 25         exit(0); // 进程结束
   26     }
   27 }
   28 
   29 void ChangeData()
   30 {
   31     int i = 0;                                                         
   32     for(; i < NUM; i++)
   33     {
E> 34         data[i] = i + rand();
   35     }
   36     printf("origin data: ");
   37     for(i = 0; i < NUM; i++)
   38     {
   39         printf("%d ", data[i]);
   40     }
   41     printf("\n");
   42 }
   43 
   44 int main()
   45 {
E> 46     srand(time(NULL));
   47     while(1)
   48     {
   49         // 修改
E> 50         ChangeData();
   51         // 备份
   52         Backup();
   53         sleep(5);
   54     }
   55 }

2.进程终止

进程终止本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

2.1退出场景

正常退出:代码跑完,运算结果正确;代码跑完,结果不正确。

异常退出:代码没跑完,出现异常(比如除零错误)(收到了信号,导致异常)。

退出信号举例:

2.2常见退出方法

从main返回;调用exit;_exit

2.2.1退出码

int main() { return 0; } ,0表示进程的退出码,表示进程执行情况。0表示成功,非0表示失败。退出码是给父进程的,如:bash或者自定义父进程。

退出码可以让父进程知道子进程将任务完成的结果如何。

示例:

关于$?:?是一个变量名,保存的是bash命令行上运行的最近一个退出进程的退出码

常见退出码:

2.2.2strerror

将错误码转换为字符串描述的函数。

部分结果:

2.2.3错误码和退出码区别

维度 错误码 退出码
粒度 细粒度(函数级) 粗粒度(进程级)
目的 诊断具体错误原因 表示整体成功/失败
使用者 程序员(调试/处理) 系统/父进程(流程控制)
时间点 运行时 程序结束时
持久性 临时,会被覆盖 最终,传递给父进程
标准化 POSIX定义部分 Shell惯例为主

核心区别 :错误码用于程序内部 的错误诊断和处理,而退出码用于进程间的成功/失败通信。一个程序内部可以使用多种错误码,但最终只能有一个退出码来总结整个程序的执行结果。

衡量一个进程运行结果是否"可信",其实可以用两个数字表示:exit code、signal number,当一个进程出现异常了,退出码没有意义:signal number != 0, exit code无意义。而这两个数字会出现在僵尸进程的pcb中。

2.2.4进程正常退出

1.main函数中return

在函数中调用return是指函数调用结束,在main中调用return是指进程结束

2.调用exit()

int status就是指退出码

结果:

可见,任意地方调用exit都表示进程结束。

3.调用_exit()

其实践结果与exit()相同,说明:虽然status是int,但是仅有低8位可以被⽗进程所用。_exit(-1)时,在终端执⾏$?发现返回值是255。

但是建议使用exit(),因为exit最后也会调用_exit,但在调用_exit之前,还做了其他工作:

1.执行用户通过 atexit或on_exit定义的清理函数。

2.关闭所有打开的流,所有的缓存数据均被写入

3.调用_exit

复制代码
int main()
{
 printf("hello");
 exit(0);
}
运⾏结果:
[root@localhost linux]# ./a.out
hello[root@localhost linux]#
int main()
{
 printf("hello");
 _exit(0);
}
运⾏结果:
[root@localhost linux]# ./a.out
[root@localhost linux]# 

3.进程等待

3.1进程等待必要性

由于子进程退出,父进程如果不管不顾,就可能造成'僵尸进程"的问题,进而造成内存泄漏。

并且,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kil-9也无能为力,因为谁也没有办法杀死一个已经死去的进程。

最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是

不对,或者是否正常退出。

父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3.2进程等待的方法

3.2.1wait方法

复制代码
返回值:
 成功返回被等待进程pid,失败返回-1。
参数:
 输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL

解决僵尸问题:

复制代码
 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <time.h>
 4 #include <stdlib.h>
 5 #include <string.h>
 6 #include <sys/wait.h>
 7 #include <sys/types.h>
 8 
 9 int main()
 10{
 11     printf("父进程:pid: %d, ppid: %d\n", getpid(), getppid());
 12 
 13     pid_t id = fork();
 14     if(id < 0)        
 15     {         
 16         perror("fork");
 17         exit(1);                      
 18     }                                  
 19     if(id == 0) 
 20     {                                  
 21         int cnt = 5;                  
 22         while(cnt)   
 23         {          
 24             printf("子进程:pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt); 
 25             sleep(1);
 26             cnt--;   
 27         }         
 28         printf("子进程退出\n");
 29         exit(10);              
 30     }                                                                    
 31 
 32     sleep(10);
 33               
 34     // 父进程
 35              
 36     pid_t rid = wait(NULL);
 37     if(rid > 0)
 38     {
 39         printf("等待子进程成功\n");
 40     }
 41 
 42     sleep(5);
 43 
 44     return 0;
 45 }

结论:

1.原则上,一般都是要保证父进程最后退出

2.父进程要通过wait等待子进程

3.如果子进程不退出,父进程就会阻塞在wait这里,等待子进程死亡

3.2.2waitpid方法

通过pid_t pid可以让父进程获取子进程退出信息。options是等待方式的获取:1.默认阻塞等待2.非阻塞

复制代码
返回值:
 当正常返回的时候waitpid返回收集到的⼦进程的进程ID;
 如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;
 如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:
 pid:
 Pid=-1,等待任⼀个⼦进程。与wait等效。
 Pid>0.等待其进程ID与pid相等的⼦进程。
 status: 输出型参数
 WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)
 WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
 options:默认为0,表⽰阻塞等待
 WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

如果不存在该子进程,则立即出错返回。

waitpid(-1,status,0) == wait(status)

测试代码:

复制代码
  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <time.h>
  4 #include <stdlib.h>
  5 #include <string.h>
  6 #include <sys/wait.h>
  7 #include <sys/types.h>
  8 
  9 int main()
 10 {
 11     printf("父进程:pid: %d, ppid: %d\n", getpid(), getppid());
 12 
 13     pid_t id = fork();
 14     if(id < 0)
 15     {
 16         perror("fork");
 17         exit(1);
 18     }
 19     if(id == 0)
 20     {
 21         int cnt = 5;
 22         while(cnt)
 23         {
 24             printf("子进程:pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
 25             sleep(1);
 26             cnt--;
 27         }
 28         printf("子进程退出\n");
 29         exit(1);
 30     }
 31                                                                                        
 32     // 父进程
 33     // pid_t rid = wait(NULL);
 34     int status = 0;
 35     pid_t rid = waitpid(id, &status, 0);
 36     if(rid > 0)
 37     {
 38         printf("等待子进程成功, status: %d\n", status);
 39     }
 40 
 41     return 0;
 42 }

运行结果发现status并没有获得退出码1:

3.2.3获取子进程status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传NULL,表示不关心子进程的退出状态信息。

否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

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

status当中有32为比特位,其中高16位我们不使用,又将低16位分为次低8位和低8位,其中次低8位保存退出码,第七位表示退出信号,其中一位表示core dump标志。

所以对代码做以下修改:

那么为什么退出码为1时,status为256?

因为退出码为1,也就是说明第八位为1,而其后有8个0跟着,2^8也就是256。

结论:退出码的取值范围:0,255

提取退出信号:

父进程获取子进程的退出信息 = 退出码 + 退出信号

3.2.4如何获取到子进程退出信息

1.子进程退出的退出信息,是维护在子进程的PCB中(包括将自己设置为僵尸)

2.父进程wait子进程,本质就是去读取子进程PCB内部的退出信息

总结:检查子进程z状态,获取子进程task_struct内部记录的子进程退出信息数据。

3.2.5waitpid中的options

0:阻塞等待。只要是内核数据结构,内部就会有维护队列,所以当等待时就是将PCB投递到要被等待对象的数据队列里,状态由R变为S

WNOHANG:宏,表示非阻塞等待

阻塞等待过程中,父进程会卡在指定位置,什么都不做,非阻塞等待过程中父子进程之间会出现非阻塞轮询过程,并且函数调用之后会立即返回。

非阻塞轮询代码:其中有用于测试的野指针

复制代码
  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <time.h>
  5 #include <stdlib.h>
  6 #include <string.h>
  7 #include <sys/wait.h>
  8 #include <sys/types.h>
  9 int main()
 10 {
 11     printf("父进程: pid: %d, ppid: %d\n", getpid(), getppid());
 12     pid_t id = fork();
 13     if(id < 0)
 14     {
 15         perror("fork");
 16         exit(1);
 17     }
 18     if(id == 0)
 19     {
 20         int cnt = 5;
 21         while(cnt)
 22         {
 23             printf("子进程: pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
 24             sleep(5);
 25             cnt--;
 26             int *p = NULL;
 27             *p = 100;
 28         }
 29         printf("子进程退出\n");
 30         exit(0);
 31     }                                                                                                                                                    
 32     
 33     while(1)
 34     {
 35         //父进程
 36         int status = 0;
 37         pid_t rid = waitpid(id, &status, WNOHANG);
 38         if(rid > 0)
 39         {
 40             printf("等待子进程成功,who: %d, status:%d, exit code: %d, exit sig: %d\n", rid, status, (status>>8)&0xFF, status & 0x7F);
 41             break;
 42         }
 43         else if(rid == 0)
 44         {
 45             sleep(1);
 46             printf("子进程还没有退出,父进程轮询\n");
 47         }
 48         else 
 49         {
 50             printf("等待子进程失败,who: %d, status:%d\n", rid, status);
 51             break;
 52         }
 53     }
 54     return 0;
 55 }

在父进程等待时可以做它自己的事情,代码:

复制代码
  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <time.h>
  5 #include <stdlib.h>
  6 #include <string.h>
  7 #include <sys/wait.h>
  8 #include <sys/types.h>
  9 
 10 void PrintLog()
 11 {
 12     printf("我要打印日志!\n");
 13 }
 14                                                                                                                                                          
 15 void SyncMySQL()
 16 {
 17     printf("我要访问数据库!\n");
 18 }
 19 
 20 void Download()
 21 {
 22     printf("我要下载核心数据\n");
 23 }
 24 
 25 typedef void(*task_t)();
 26 
 27 task_t tasks[3] = {
 28     PrintLog,
 29     SyncMySQL,
 30     Download
 31 };
 32 
 33 int main()
 34 {
 35     printf("父进程: pid: %d, ppid: %d\n", getpid(), getppid());
 36     pid_t id = fork();
 37     if(id < 0)
 38     {
 39         perror("fork");                                                                                                                                  
 40         exit(1);
 41     }
 42     if(id == 0)
 43     {
 44         int cnt = 5;
 45         while(cnt)
 46         {
 47             printf("子进程: pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
 48             sleep(5);
 49             cnt--;
 50             int *p = NULL;
 51             *p = 100;
 52         }
 53         printf("子进程退出\n");
 54         exit(0);
 55     }
 56     
 57     while(1)
 58     {
 59         //父进程
 60         int status = 0;
 61         pid_t rid = waitpid(id, &status, WNOHANG);
 62         if(rid > 0)
 63         {
 64             printf("等待子进程成功,who: %d, status:%d, exit code: %d, exit sig: %d\n", rid, status, (status>>8)&0xFF, status & 0x7F);
 65             break;
 66         }
 67         else if(rid == 0)
 68         {
 69             sleep(1);
 70             printf("子进程还没有退出,父进程轮询\n");
 71             for(int i = 0; i < 3; i++)
 72             {
 73                 tasks[i]();
 74             }
 75         }
 76         else 
 77         {
 78             printf("等待子进程失败,who: %d, status:%d\n", rid, status);
 79             break;
 80         }
 81     }
 82     return 0;
 83 }

在上述代码中,若想获得退出码,还需要我们进行位操作才可以,这样的做法显然太过麻烦,此后我们使用这两个宏,也可以完成这样的状态:

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

WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)

将上述代码中野指针取消掉:

4.编写一个一次等待多个子进程的代码

复制代码
  1 #include <iostream>
  2 #include <cstdlib>
  3 #include <cstdio>
  4 #include <unistd.h>
  5 #include <string>
  6 #include <vector>
  7 #include <sys/types.h>
  8 #include <sys/wait.h>
  9 
 10 const int gnum = 5;
 11 
 12 
 13 void Work()
 14 {
 15     int cnt = 5;
 16     while(cnt)
 17     {
 18         printf("%d work..., cnt: %d\n", getpid(), cnt--);
 19         sleep(1);
 20     }
 21 }
 22 
 23 int main()
 24 {
 25     std::vector<pid_t> subs;
 26     for(int idx = 0; idx < gnum; idx++)
 27     {
 28         pid_t id = fork();
 29         if(id < 0)
 30             exit(1);
 31         else if(id == 0)                                                                                                                                 
 32         {
 33             //child
 34             Work();
 35             exit(0);
 36         }
 37         else
 38         {
 39             subs.push_back(id);
 40         }
 41     }
 42 
 43     for(auto &sub : subs)
 44     {
 45         int status = 0;
 46         pid_t rid = waitpid(sub, &status, 0);
 47         if(rid > 0)
 48         {
 49             if(WIFEXITED(status))
 50             {
 51                 printf("child quit normal, exit code: %d\n", WEXITSTATUS(status));
 52             }
 53             else
 54             {
 55                 printf("%d child quit error!\n", sub);
 56             }
 57         }
 58     }
 59 
 60 
 61     return 0;
 62 }

本章完。

相关推荐
大树887 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠7 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质7 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush47 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5207 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz8 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工8 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智9 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩9 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_9 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化