进程控制详解

🎬 个人主页Vect个人主页

🎬 GitHubVect的代码仓库
🔥 个人专栏 : 《数据结构与算法》《C++学习之旅》《Linux

⛺️Per aspera ad astra.



文章目录

1. 进程创建

fork函数

根据文档描述:

fork函数的作用是创建一个子进程

返回值:

  • >0 把子进程的pid返回给父进程
  • ==0 说明创建的是子进程
  • -1 说明进程创建失败

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

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

看如下代码:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(){
  printf("before: pid: %d\n",getpid());

  pid_t id = fork();
  if(id < 0){
    perror("fork");
    return 1;
  }else if(id == 0){
    printf("after: child, pid:%d, return val: %d\n",getpid(),id);
  }else{
    printf("after: fatehr, pid:%d, return val: %d\n",getpid(),id);
  }

  return 0;
}

输出结果:

bash 复制代码
[vect@VM-0-11-centos process_control]$ make
gcc -o show_fork show_fork.c
[vect@VM-0-11-centos process_control]$ ./show_fork
before: pid: 9928
after: fatehr, pid:9928, return val: 9929
after: child, pid:9929, return val: 0

frok之前,父进程执行打印,此时没有创建子进程

fork之后,父子进程分别执行各自的打印操作

也就是说,fork之前父进程独立执行,fork之后父子进程执行流分别执行

为什么子进程返回0,父进程返回子进程的pid?

一个父进程可以有多个子进程,而一个子进程只能有一个父进程。对于父进程,子进程需要被标记,父进程可以根据子进程的pid返回值更好的管理子进程;而对于子进程来说,父进程无需被标记

为什么fork有两个返回值?

并不是fork返回两次,而是fork调用一次会创建一个子进程,父子进程会从fork返回处继续执行,各自拿到一个返回值

写时拷贝

fork创建子进程时,内核不会立刻复制父进程的物理内存,而是做两件事:

  • 共享物理内存: 父子进程共享同一块物理内存空间(包括代码和数据)
  • 标记页表项为 "只读":将父子进程页表中指向该共享物理内存的页表项权限均设置为 "只读";同时保证父子进程的虚拟地址通过各自的页表项,映射到同一块物理内存页

当父进程或子进程试图修改这块内存 时,内核触发缺页异常 , 进行缺页中断,随即开始检测,判定有进程要发生写时拷贝------为要修改的的内存创建独立的物理内存副本,然后让修改方指向这个新副本,另一方仍指向原内存

详细过程如下:

  1. fork 调用,创建子进程

    • 父进程调用fork,内核创建子进程的PCB,复制父进程的页表
    • 内核将父子进程页表项的代码和数据标记为"只读"
    • 父子进程的页表映射到同一块物理内存
    • 返回两个进程的返回值,此时父子进程完全共享内存
  2. 无修改操作->持续共享内存

    • 若父子进程都只读取内存,永远不触发写实拷贝
  3. 有操作修改->触发写时拷贝

    当父子进程试图写入某块内存时:

    • CPU触发缺页异常
    • 内核捕获异常,检查是写时拷贝导致的异常
    • 内核为被修改的这个内存分配新的物理内存
    • 内核将原内存的内容拷贝到新的内存
    • 修改触发写操作的页表项,指向新的物理内存
    • 取消新内存的只读标记,原内存保持只读
    • 异常处理结束,CPU重新执行写操作,此时写入新副本

fork的用法

  • 一个父进程希望复制自己,使父子进程执行不同的代码段
  • 一个进程要执行一个不同的程序

fork失败的原因

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

2. 进程终止

进程终止的本质是释放内核数据结构和对应代码与数据

有三个场景:

  • 代码运行完毕,结果正确->返回0
  • 代码运行完毕,结果错误->返回非0
  • 代码异常终止->本质是OS用信号提前终止了进程

进程退出方法

正常终止的进程可以通过echo $?查看最近一次进程的退出码

bash 复制代码
./look_exit
hh
[vect@VM-0-11-centos process_control]$ echo $?
0
[vect@VM-0-11-centos process_control]$ lsls
-bash: lsls: command not found
[vect@VM-0-11-centos process_control]$ echo $?
127
  • main函数返回
  • 调用exit
  • _exit

异常退出:

ctrl+c,信号终止

退出码

退出码可以告诉我们最后一次执行的进程的状态。在命令结束后,可以根据退出码知道进程以什么方式终止

Linux Shell中的退出码有

bash 复制代码
0:Success
1:Operation not permitted
2:No such file or directory
3:No such process
4:Interrupted system call
5:Input/output error
6:No such device or address
7:Argument list too long
8:Exec format error
9:Bad file descriptor
10:No child processes
11:Resource temporarily unavailable
12:Cannot allocate memory
13:Permission denied
14:Bad address
15:Block device required
16:Device or resource busy
17:File exists
18:Invalid cross-device link
19:No such device
20:Not a directory
21:Is a directory
22:Invalid argument
23:Too many open files in system
24:Too many open files
25:Inappropriate ioctl for device
26:Text file busy
27:File too large
28:No space left on device
29:Illegal seek
30:Read-only file system
31:Too many links
32:Broken pipe
33:Numerical argument out of domain
34:Numerical result out of range
35:Resource deadlock avoided
36:File name too long
37:No locks available
38:Function not implemented
39:Directory not empty
40:Too many levels of symbolic links
41:Unknown error 41
42:No message of desired type
43:Identifier removed
44:Channel number out of range
45:Level 2 not synchronized
46:Level 3 halted
47:Level 3 reset
48:Link number out of range
49:Protocol driver not attached
50:No CSI structure available
51:Level 2 halted
52:Invalid exchange
53:Invalid request descriptor
54:Exchange full
55:No anode
56:Invalid request code
57:Invalid slot
58:Unknown error 58
59:Bad font file format
60:Device not a stream
61:No data available
62:Timer expired
63:Out of streams resources
64:Machine is not on the network
65:Package not installed
66:Object is remote
67:Link has been severed
68:Advertise error
69:Srmount error
70:Communication error on send
71:Protocol error
72:Multihop attempted
73:RFS specific error
74:Bad message
75:Value too large for defined data type
76:Name not unique on network
77:File descriptor in bad state
78:Remote address changed
79:Can not access a needed shared library
80:Accessing a corrupted shared library
81:.lib section in a.out corrupted
82:Attempting to link in too many shared libraries
83:Cannot exec a shared library directly
84:Invalid or incomplete multibyte or wide character
85:Interrupted system call should be restarted
86:Streams pipe error
87:Too many users
88:Socket operation on non-socket
89:Destination address required
90:Message too long
91:Protocol wrong type for socket
92:Protocol not available
93:Protocol not supported
94:Socket type not supported
95:Operation not supported
96:Protocol family not supported
97:Address family not supported by protocol
98:Address already in use
99:Cannot assign requested address
100:Network is down
101:Network is unreachable
102:Network dropped connection on reset
103:Software caused connection abort
104:Connection reset by peer
105:No buffer space available
106:Transport endpoint is already connected
107:Transport endpoint is not connected
108:Cannot send after transport endpoint shutdown
109:Too many references: cannot splice
110:Connection timed out
111:Connection refused
112:Host is down
113:No route to host
114:Operation already in progress
115:Operation now in progress
116:Stale file handle
117:Structure needs cleaning
118:Not a XENIX named type file
119:No XENIX semaphores available
120:Is a named type file
121:Remote I/O error
122:Disk quota exceeded
123:No medium found
124:Wrong medium type
125:Operation canceled
126:Required key not available
127:Key has expired
128:Key has been revoked
129:Key was rejected by service
130:Owner died
131:State not recoverable
132:Operation not possible due to RF-kill
133:Memory page has hardware error

我们用这段代码可以获得:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(){
  for(int i = 0; i < 200; i++){
    printf("%d:%s\n",i,strerror(i));
  }
  return 0;
}

进程正常退出

main函数的return

main函数中使用return退出进程是我们最常用的方法,但是这里需要注意:

普通函数的return只代表这个函数结束,并不代表整个进程结束

_exit函数和exit函数

_exitexit可以在代码的任何地方退出

我们先来看区别:

分析之前我们需要搞清楚标准IO缓冲区

缓冲区的本质: 标准IO库为了减少系统调用(写道中断/文件是昂贵的内核操作),在用户语言层面开辟的一块内存区域,暂时存放输出数据,满足条件时才把数据刷到内核(最终输出到终端/文件)

分析:

exit标准C库函数 ->用户语言层面,_exit系通调用 ->内核层面,二者核心差异是是否执行用户语言层面的清理操作,尤其是缓冲区刷新

很显然,exit会刷新完缓冲区再退出,而_exit直接退出

所以,我们现在所说的缓冲区是用户语言级别的!!!不在OS内部!!!

所以,对于用户级别的exit是封装了内核级别的_exit


3.进程等待

进程等待的必要性

  • 子进程退出,变成僵尸态,等待父进程处理,若不进行处理,可能会造成内存泄漏
  • 进程一旦变成僵尸态,kill -9也无能为力,谁也没办法杀死一个已经死去的进程
  • 父进程拍给子进程的任务的完成情况,需要反馈

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

获取子进程的status

子进程的status就是退出码,仅仅从终止后,向父进程返回的一个整数值,它的作用是:

  • 子进程是正常退出 or异常终止
  • 正常退出,结果是否正确,异常终止,终止原因是什么

Linux下status的二进制结构

status是一个16位的整数,位段划分如下图:

  • 正常退出:高8位有效,低8位为0
  • 信号终止:低8位有效,高8位无效

我们可以通过位运算的方式得到退出码:

  • exit_code = (status >> 8) & 0xff;

    退出码在高8存储,且退出码的范围是0,255,需要将这8位数右移到低8位,然后只保留低8位,则按位与0xff->

    1111 1111

  • exit_signal = status & 0x7f;

    信号存在低7位,保留低7位即可,按位与0x7f->111 1111

对此,系统提供了两个宏来获取退出码和退出信号:

cpp 复制代码
#define WIFEXITED(status)  (((status) & 0x7F) == 0)

#define WEXITSTATUS(status) (((status) >> 8) & 0xFF)

需要注意的是:一个进程非正常退出,说明是被信号终止,那么该进程的退出码就没有意义了

进程等待方法

wait

cpp 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int* status);

作用:

​ 等待任意子进程

返回值:

​ 成功返回:被等待进程的pid 失败返回:-1

参数:

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

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main(){
  pid_t id = fork();
  if(id == 0){
    // 子进程
    int count = 6;
    while(count--){
      printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
      sleep(1);
    }
    exit(0);
  }

  // 父进程
  int status = 0;
  // pid_t wait(int* status);
  pid_t ret = wait(&status);  // 等待子进程 获取子进程返回码
  if(ret > 0){
    // 等待成功
    printf("successful wait\n");
    if(WIFEXITED(status)){ // 判断是否正常退出
      printf("exit code: %d\n",WEXITSTATUS(status));  // 提取退出码
    }
  }
  sleep(3);

  return 0;
}

输出结果:

bash 复制代码
[vect@VM-0-11-centos process_control]$ gcc -o wait wait.c
[vect@VM-0-11-centos process_control]$ ./wait
I am child, pid: 4197, ppid: 4196
I am child, pid: 4197, ppid: 4196
I am child, pid: 4197, ppid: 4196
I am child, pid: 4197, ppid: 4196
I am child, pid: 4197, ppid: 4196
I am child, pid: 4197, ppid: 4196
successful wait
exit code: 0

waitpid

cpp 复制代码
pid_t waitpid(pid_t pid, int* status, int options)

作用:

​ 等待指定子进程或任意子进程

返回值:

  • 正常返回时返回子进程pid
  • 若设置了选项WNOHANG,而调用中waitpid中发现没有已经退出的子进程可以收集,返回0
  • 调用出错,返回-1,这时errno会被设置成相应的值指示错误

参数:

  • PID:

    • pid=-1,等待任意一个子进程,与wait等效
    • pid>0,等待这个pid的子进程
  • status:

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

  • options:

    当设置成WNOHANG时,若等待的子进程没有结束,直接返回0,不等待;若正常结束,返回该子进程的pid

例如,创建子进程后,父进程可以一直等待子进程,waitpid的第三个参数设置为0,指导子进程退出后读取子进程的退出信息

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main(){
  pid_t id = fork();
  if(id == 0){
    // 子进程
    int count = 10;
    while(count--){
      printf("I am child, pid:%d, ppid:%d\n",getpid(),getppid());
      sleep(1);
    }
    exit(0);
  }

  // 父进程
  int status = 0;
  pid_t ret = waitpid(id, &status, 0);
  if(ret >= 0){ // 等待成功
    printf("wait successfully\n");
    if(WIFEXITED(status)){    // 正常返回
      printf("exit code: %d\n",WEXITSTATUS(status));
    }else{  // 异常返回 被信号杀死
      printf("killed by signal, %d\n",status & 0x7F);
    }
  }
  sleep(3);

  return 0;

}

进程被杀信号杀死,也可以成功等待子进程

注意:被信号杀死的进程,其退出码没有意义

阻塞等待与非阻塞等待

阻塞等待方式

当子进程未退出时,父进程一直在等待子进程,等待期间,父进程不能做任何事情,这种等待方式称为阻塞等待

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

int main(){
  pid_t pid = fork();
  if(pid < 0){
    printf("fork err");
    exit(1);
  }
  else if(pid == 0){  // 子进程
    printf("I am child, pid: %d\n",getpid());
    sleep(5);
    exit(257);
  }else{
    int status = 0;
    pid_t ret = waitpid(-1,&status,0);  // 阻塞等待,等待5s
    printf("waiting...\n");
    if(WIFEXITED(status) && ret == pid){
      printf("wait child 5s successfully, child return code: %d\n",WEXITSTATUS(status)); 
    }else{
      printf("wait child failed\n");
      exit(1);
    }
  }
  return 1;
}

输出结果:

bash 复制代码
[vect@VM-0-11-centos process_control]$ gcc -o block_wait block_wait.c
[vect@VM-0-11-centos process_control]$ ./block_wait 
I am child, pid: 17498
waiting...
wait child 5s successfully, child return code: 1

非阻塞等待的轮询检测方式

而实际上,我们完全没有必要让父进程干等着,父进程可以在等待子进程的同时做其他事情------非阻塞等待

waitpid的第三个参数传WNOGANG,若等待的子进程没有结束,那么waitpid函数直接返回0,不等待;若子进程正常结束,返回该子进程的pid

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

int main(){
  pid_t id = fork();
  if(id < 0){
    perror("fork err\n");
    exit(1);
  }else if(id == 0){  // child
    int count = 5;
    while(count--){
      printf("child runing... pid: %d\n",getpid());
      sleep(3);
    }
    exit(0);
  }else{  // father
    while(1){
      int status = 0;
      pid_t ret = waitpid(id,&status,WNOHANG);
      if(ret > 0){
        printf("wait child successfully\n");
        printf("child exit code:%d\n",WEXITSTATUS(status));
        break;
      }else if(ret == 0){
        printf("father does other things...\n");
        sleep(1);
      }else{
        printf("wait err...\n");
        break;
      }
    }
  
  }

  return 0;
}

父进程每隔一段时间查看子进程是否退出;若为退出,父进程忙自己的事情,每隔一段时间来查看一次,直到子进程退出读取其退出信息


4. 进程程序替换

替换原理

fork创建子进程后,子进程执行的是和父进程相同的程序(可能执行不同的代码分支),想要让子进程执行另一个程序,需要调用函数完成->exex系列函数

当进程调用exec系列函数时,该进程的空间、代码和数据完全被新程序替换,并从新程序的启动历程开始执行

当进程替换时,有没有创建新的进程?

进程程序替换之后,该进程对应的PCB、进程地址空间及页表等都没有发生改变,仅仅改变进程在物理内存中的代码和数据,所以没有创建新的进程,并且进程程序替换前后该进程的pid没有改变

bash 复制代码
ls
block_wait.c  Makefile         other              show_fork.c
exec.c        myexec           other.c            wait.c
exit.c        no_block_wait.c  print_exit_code.c  waitpid.c
[vect@VM-0-11-centos process_control]$ make
gcc -o myexec exec.c
[vect@VM-0-11-centos process_control]$ ./myexec 
我是 exec, pid: 22591
我是other, pid: 22591[vect@VM-0-11-centos process_control]$ 

pid相同,所以没有创建新的进程

子进程进行程序替换后,会影响父进程的代码和数据吗?

不会,子进程刚被创建,和父进程共享代码和数据,但当子进程进程程序替换时,就意味着子进程需要对其数据和代码进行写入操作,这时候发生写时拷贝,此后父子进程代码和数据分离

替换函数

有六种以exec 开头的函数,统称为exec函数

cpp 复制代码
#include <unistd.h>

extern char **environ;

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[]);

函数解释

  • 调用成功,不用返回
  • 调用出错,返回-1

所以,exec只要返回了,就是调用失败

怎么理解这些函数

  • l(list):参数采用列表
  • v(vector):参数用数组
  • p(path):有p则自动搜索环境变量PATH,不用带详细路径
  • e(env):表示自己维护环境变量

函数使用

cpp 复制代码
execl("/bin/ls","ls","-al","--color",NULL);

第一个参数path是带路径的指令,想执行谁,第二个参数是可变参数列表,想要怎么执行

cpp 复制代码
char* argv[] = {"ls","-l",NULL};
execv("/bin/ls",argv);

第一个参数path是带路径的指令,想执行谁,第二个参数是char*的数组,想要怎么执行

这个和命令行参数的第二个参数一样的形式,都是一张表

cpp 复制代码
execlp("ls","ls","--color",NULL);   

第一个参数是不带路径的命令,想执行谁,第二个参数是可变参数列表,想要怎么执行。这里同时出现两个参数ls,表示的语义不一样,参数位置也不一样

cpp 复制代码
char* argv[] = {"ls","-l",NULL};
execvp("ls",argv);

这里不做解释了

cpp 复制代码
char* argv[] = {"ls","-l",NULL};
execvpe("ls",argv,NULL);

第一个参数是不带路径的命令,想执行谁,第二个参数是char*的数组,想要怎么执行,第三个参数是新的环境变量

对于环境变量:

  • 不传参:会默认继承父进程的环境变量表
  • 手动传参:会用全新的环境变量
  • 单纯新增环境变量:使用putenv函数,谁调用了putenv,就把putenv里的环境变量导入到调用者的环境变量表

一般来说,我们不需要手动传环境变量,系统默认的就够了,可以得到一个结论:程序替换是不影响命令行参数和环境变量的

cpp 复制代码
int execve(const char *path, char *const argv[], char *const envp[]);

第一个参数是要执行程序的路径,想执行谁,第二个参数是一个char*数组,想要怎么执行,第三个参数是新的环境变量

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

相关推荐
groundhappy3 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕4 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫5 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生6 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.117 小时前
linux基础IO
linux·运维·服务器
BelongPanda9 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯9 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_777998349 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器
yunwei379 小时前
eBPF 教程:检查 exec 后真正安装的可执行镜像
linux·开源
笑锝没心没肺10 小时前
yum update 报错:GPG key retrieval failed: [Errno 14] HTTPS Error 404 - Not Found
linux·运维·服务器