进程管理:创建、终止、等待、替换

进程控制

一、进程创建

1. 通过fork()函数创建新进程

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

调用fork函数后,系统做的:

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

2.fork的常见用法

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

3.fork调用失败

  1. 系统中有太多的进程
  2. 实际用户的进程数超多了限制(少见)

二、写实拷贝(Copy‑On‑Write,COW)

fork()时发生什么?

  1. 内核创建子进程:

    • 新 task_struct(进程控制块)

    • 新 mm_struct(虚拟地址空间)

    • 复制父进程页表 ,但不复制物理内存

  2. 所有共享页的权限 设为只读

  3. 父子进程:

    • 虚拟地址完全一样

    • 物理地址完全共享

    • 只能读,不能写

什么时候真正拷贝?(写触发)

只要任何一方(父 / 子)对共享页执行写操作

  1. CPU 检测到页表标志位只读 → 触发缺页异常(Page Fault),写不进去
  2. 内核判断是 COW 场景:
    • 分配新物理页
    • 旧页内容复制到新页
    • 修改当前进程页表:指向新页 → 设为可写
    • 另一进程继续共享原页
  3. 结果:只复制 "被写的那一页",不是全量拷贝

三、进程终止

三种结果

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

进程终止有三种结果

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止

进程main函数返回的相当于一个进程退出码 ;在Linux中,可以通过命令echo $?将最近一次进程运行的退出码打印在屏幕上。

一个进程正常退出,就是main函数返回0 ;除了返回01,其他返回值都代表着不成功。1一般是程序员自己定义的通用错误。


错误码与退出码的区别

1. 退出码(进程退出状态码)

全称:进程退出码 exit code

  • 作用:一个进程结束运行后,留给父进程看的运行结果
  • 来源:
    1. 程序里 exit(数值)
    2. main 函数 return 数值
    3. 被信号杀死由内核赋值
  • 范围:0~255 ,只占低 8 位
2. 错误码(系统调用错误码 errno)
  • 作用:调用 Linux 内核函数失败时,标记失败原因
  • 来源:open/read/write/fork/pipe系统调用执行失败
  • 本质:全局整型变量 int errno
  • 范围:几十~上百个宏值(EPERM、ENOENT、EINTR...)
  • 头文件:<errno.h>
  • 特点:只有调用失败才赋值,成功不清空
3.核心五大区别
  1. 所属主体不同

    • 退出码:整个进程跑完后的最终结果

    • 错误码:某一次系统调用失败的原因

  2. 使用时机不同

    • 退出码:进程彻底结束之后使用

    • 错误码:程序运行中途,调用函数出错立刻查看

  3. 取值范围不同

    • 退出码:0~255

    • 错误码:多枚举宏,数量极多

  4. 获取方式不同

    • 退出码:父进程 wait 读取子进程状态

    • 错误码:程序内部直接读全局变量 errno

4.直观例子

例子 1:退出码

c 复制代码
int main()
{
    return 0;  // 退出码 0 成功
}

Shell 执行:echo $? 查看上一条命令退出码

例子 2:错误码

c 复制代码
int fd = open("不存在的文件.txt", O_RDONLY);
if(fd < 0)
{
    printf("%d\n", errno); // 打印错误码,代表文件不存在
}
5.最容易混淆的点
  1. 退出码可以自己随便设,errno 是内核固定规定

  2. 一个进程只有一个最终退出码,但运行中能产生无数次 errno 错误码

  3. $? 拿到的是退出码 ,不是 errno

  4. 程序调用系统调用出错 → 产生 errno

    程序结束运行 → 给出 退出码


三种结果的表示

Linux中,进程终止有不同信号:

(1)代码跑完,结果正确

运行期间,没有收到信号0 && return 0 -> signumber:0 && 退出码:0

(2)代码跑完,结果错误

signumber:0 && 退出码 !0;

(3)代码没跑完,进程异常。

signumber:!0

此时退出码已经没有意义了,此时关注的就是什么原因 导致的异常!Linux中就是被信号终止了

所以

一个进程执行的结果状态,可以用两个数字表示:int sigint exit_code。用户不需要维护这些,当一个进程结束时,OS会把进程退出的详细信息写入到进程的task_struct结构体中!!!那么 ,进程退出,需要僵尸维护自己的退出状态!

不考虑进程异常,如何退出进程

  1. main函数return
  2. 在任意地方exit()

函数exit()_exit()的区别

一、本质区别

  1. exit()
    • 属于 C 标准库函数(封装了系统调用)
    • 作用:正常、优雅地终止进程
    • 会做大量清理工作
  2. _exit()
    • 属于 Linux 系统调用
    • 作用:立即、暴力终止进程
    • 不做任何清理工作
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    printf("Hello"); // 没有 \n,数据在缓冲区里

    // _exit(0);   // 用这个 → 什么都不输出!
    exit(0);      // 用这个 → 会输出 Hello
}


二、最关键的区别(3点)

  1. 是否刷新 I/O 缓冲区

    • exit ():会刷新

    • _exit ():不刷新

  2. 层级不同

    • exit () = 库函数(上层)

    • _exit () = 系统调用(底层)

  3. 使用场景

    • exit ():正常程序退出

    • _exit ():子进程在 fork 后 exec 前使用(防止缓冲区混乱)


四、进程等待

进程等待的必要性

  1. 回收子进程资源 :子进程退出后若父进程不等待,会变成僵尸进程,占用进程号等系统资源,造成资源泄漏。
  2. 获取子进程退出状态 :父进程可通过等待拿到子进程退出码 ,判断子进程是正常结束、异常终止还是被信号终止。
  3. 控制父子进程执行顺序 :让父进程阻塞等待子进程完成任务后再继续执行,实现业务逻辑先后次序。
  4. 避免孤儿进程 :防止父进程先退出,子进程被 init 进程接管,导致进程管理混乱。
  5. 保证数据交互完整性:确保子进程读写、运算等任务执行完毕,父进程再读取其运行结果。

等待方法

当父进程还在进行,而子进程结束时:

c 复制代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<error.h>
#include<stdlib.h>
int main() {
	pid_t id = fork();
	if(id == 0) {
		int cnt = 5;
		while (cnt--) {
			printf("我是子进程, pid: %d\n", getpid());
			sleep(1);
		}
		exit(0);
	}
	else if (id > 0) {
		while (1) {
			printf("我是父进程, pid : %d\n", getpid());
			sleep(1);
		}
	}

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

通过在父进程中添加代码:

c 复制代码
// 等待子进程
pid_t rid = wait(NULL);
if (rid == id) {
    // 等待成功
    printf("pid: %d, wait success!\n", getpid());
}

也就是说,当父进程wait子进程,但是子进程就是没有退出,则父进程会阻塞在wait函数中;

再利用sleep来直观的查看对僵尸进程的改善:

2.waitpid函数(更推荐)
c 复制代码
#include <sys/types.h>
#include <sys/wait.h>
pid_ t waitpid(pid_t pid, int *wstatus, int options);
// 当pid=-1;options=0时,函数等同于wait()

参数

  1. pid

    • pid > 0:等待指定 pid子进程

    • pid = 0:等待同组任意子进程

    • pid = -1:等待任意子进程(等价 wait)

    • pid < -1:等待指定进程组任意子进程

  2. wstatus

传出参数,存子进程退出信息,传NULL表示不关心。

常用宏:

  • WIFEXITED(w):正常退出为真

  • WEXITSTATUS(w):获取退出码

  1. options

    • 0阻塞等待(默认)

    • WNOHANG非阻塞,不等待,无子进程退出立即返回 0

返回值

  • >0:成功,返回退出子进程 pid
  • 0:非阻塞模式,子进程还没退出
  • -1:出错

wait函数换成waitpid

3.waitpid的返回参数wstatus

当我们将子进程的退出码设置为1

c 复制代码
if(id == 0) {
    int cnt = 5;
    while (cnt--) {
        printf("我是子进程, pid: %d\n", getpid());
        sleep(1);
    }
    exit(1); // 修改
}

我们知道wstatus是获取子进程退出信息的!子进程退出有三种情况,三种情况与两个数字有关;

所以wstatus本质是得到进程退出的两个数字!!!

那一个wstatus怎么得到两个数字呢?其实wstatus是有32个比特位

所以当我们需要拿到具体的退出码或者错误码时,底层用的是位移>> 按位与& ;平常使用时就用定义好的

c 复制代码
int exit_code = ((wstatus >> 8)&0xff); // 1111 1111
int exit_sig = wstatus&0x7f; // 0111 1111
c 复制代码
// 将子进程退出码改为123
exit(123);

那我们再试一试,将父子都设置成死循环,再通过kill -9杀死子进程,会发生什么

此时父进程立马回收,并显示子进程的退出信号9

4.阻塞与非阻塞

第三个参数 options 决定阻塞 / 非阻塞

(1)阻塞模式(默认)

c 复制代码
waitpid(pid, &status, 0);
  • options=0 = 阻塞等待
  • 逻辑:父进程卡死不动,一直等到指定子进程退出,函数才返回
  • 特点:父进程暂停执行,专一等子进程结束

(2)非阻塞模式

c 复制代码
waitpid(pid, &status, WNOHANG);
  • options=WNOHANG = 非阻塞
  • 逻辑:不等!立刻返回
    1. 子进程已退出:返回子进程 PID
    2. 子进程还在运行:立刻返回 0,父进程继续往下跑代码

非阻塞的用法

如果只用一次非阻塞:子进程没结束就直接跳过回收 ,容易产生僵尸进程

正确用法:循环轮询

c 复制代码
while(1)
{
    // 非阻塞查看
    pid_t ret = waitpid(-1, NULL, WNOHANG);
    if(ret > 0)
        printf("回收子进程\n");
    else if(ret == 0)
    {
        // 子进程还在跑,父进程做别的事
        printf("子进程运行中,父进程忙别的\n");
        sleep(1);
    }
    else
        break; // 没有子进程了
}

完整的测试代码:

c 复制代码
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<error.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main() {
	pid_t id = fork();
	if(id == 0) {
		int cnt = 5;
		while (cnt--) {
			printf("我是子进程, pid: %d\n", getpid());
			sleep(1);
		}
		exit(10);
	}
	else {
		while (1) {
			int wstatus = 0;
			pid_t rid = waitpid(id, &wstatus, WNOHANG);
			if (rid > 0) {
				printf("wait success,退出的子进程是: %d, exit_code: %d\n",rid, WEXITSTATUS(wstatus));
				break;
			}
			else if (rid == 0) {
				printf("子进程还在运行,父进程还得等!\n");
				sleep(2);
			}
			else {
				perror("waitpid\n");
				break;
			}
		}
	}
	return 0;
}

五、进程程序替换

1.现象

C语言头文件<unistd.h>中有一系列程序替换的相关函数exec*

c 复制代码
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[]);

试试execl函数

c 复制代码
#include<stdio.h>
#include<unistd.h>

int main() {
	printf("我变成了一个进程:%d\n", getpid());
	// 执行另一个程序
	execl("/usr/bin/ls","-a","-l",NULL);	// 程序替换函数

	printf("我的代码运行中...");
	printf("我的代码运行中...");
	printf("我的代码运行中...");
	printf("我的代码运行中...");
	return 0;
}

我们发现,此时进程没有执行之后的代码,而是执行了ls -a -l

2.原理

其中我们发现,在替换的过程中,有一个文件的IO过程,它是由OS来完成的。

(补充)在运行代码程序时,其实最开始运行的程序是一个加载器 ,加载器通过找到需要运行的目标程序,进行程序替换来运行那个目标程序。

而且一般我们用程序替换都是在子进程中替换 ;而且因为子进程需要替换,那么肯定就不能还和父进程共享数据了,此时就会发生写实拷贝

3.系列函数说明

execl、execlp、execle、execv、execvp、execve

  • l(list) : 表示参数采用列表,那么实际传参里就要NULL
  • v(vector) : 参数用数组,实际传参里可以没有NULL
  • p(path) : 有 p 自动搜索环境变量 PATH
  • e(env) : 表示自己维护环境变量,传入时用的就是自己的新的,系统的不参与
返回值规则相同
  1. 成功时:程序直接被新程序覆盖,原来代码全部没了 ,所以不会回到 exec* 后面代码,无返回值

  2. 失败时:会 return -1,用来告诉程序:启动新程序失败了,继续往下跑原来代码。

传参规则
(1)execl
c 复制代码
// 格式:execl(全路径, 程序名, 参数1, 参数2, ..., NULL);
execl("/bin/ps", "ps", "-ef", NULL);
  • 必须写绝对路径
  • 挨个写参数,末尾补NULL
(2)execlp
c 复制代码
// 格式:execlp(程序名, 程序名, 参数..., NULL);
execlp("ps", "ps", "-ef", NULL);
  • 不用全路径,自动搜 PATH
  • 其余同 execl,逐个传参
(3)execle
c 复制代码
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};

// 格式:execle(全路径, 程序名, 参数..., NULL, 自定义环境数组);
execle("/bin/ps", "ps", "-ef", NULL, envp);
  • 绝对路径 + 逐个传参
  • 最后多传一层环境变量数组
(4)execv
c 复制代码
char *const argv[] = {"ps", "-ef", NULL};

// 格式:execv(全路径, 参数字符串数组);
execv("/bin/ps", argv);

绝对路径

  • 所有参数提前放进char * 数组,数组尾存 NULL
(5)execvp
c 复制代码
char *const argv[] = {"ps", "-ef", NULL};

// 格式:execvp(程序名, 参数字符串数组);
execvp("ps", argv);
  • 搜 PATH,不用全路径
  • 参数放数组传入
(6)execve(原生系统调用)
c 复制代码
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};

// 格式:`execve(全路径, 参数数组, 环境变量数组);`
execve("/bin/ps", argv, envp);
  • 三个参数:路径、参数数组、环境数组
  • 无可变参,纯数组传参
(7)传入环境变量注意

如果传入的是自己整的一个数组比如像:char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};,函数用的时候就只会用到数组里面仅有的这些,进程不会用原有的系统环境变量;

当我们需要用到进程原有的系统环境变量 并且还要追加用自己的时,我们可以取到:

首先要将环境变量定义出来;再用函数putenv()追加自己的;

c 复制代码
char* const argv[] = {"myexe", "-a", NULL};
extern char** environ;
putenv((char*)"myenv=abcd");
execve("./myexe", argv, environ);

(补充)所以之前main函数的环境变量的参数,也是父进程通过程序替换时传入的参数。

f", NULL};

char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};

相关推荐
鹏大师运维12 小时前
信创数据库开发--SQLark这款工具支持麒麟、统信
linux·数据库·数据库开发·麒麟·统信·sqlark·桌面操作系统
hyunbar12 小时前
高级 SQL 实战教程(华为云 DWS / PostgreSQL 版)
linux·服务器·数据库
小此方12 小时前
Re:Linux系统篇(十七)进程篇·二:深入浅出 [进程概念与进程父子关系]:从底层原理到实战应用
linux·运维·驱动开发
hjjdebug12 小时前
ubuntu系统 usbmouse 驱动代码分析
linux·ubuntu·usbmouse
认真的薛薛12 小时前
Linux运维:Jenkins+Argocd
linux·运维·jenkins
windawdaysss13 小时前
使用VMware Workstation Pro安装Ubuntu虚拟机教程
linux·运维·ubuntu
宋浮檀s13 小时前
Linux后门持久化排查
linux·运维·服务器
xuhaoyu_cpp_java13 小时前
Linux学习(一)
linux·经验分享·笔记·学习
小此方13 小时前
Re: Linux系统篇(十八)进程篇·三:深度硬核!全面起底 Linux 进程状态变化与内核链表动态解绑
linux·驱动开发·链表