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

进程控制

一、进程创建

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};

相关推荐
FoldWinCard4 小时前
D5 Linux 网络及端口命令
linux·运维·服务器
零涂毕业设计5 小时前
毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享
linux·stm32·单片机·嵌入式·esp32·fpga·oled
tedcloud1235 小时前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
霸道流氓气质6 小时前
KMS 密钥管理服务(Key Management Service)原理与实践
linux·服务器·数据库
SakitamaX6 小时前
LVS(Linux Virtual Server)概念详解
linux·运维·lvs
小张同学a.6 小时前
Linux系统管理
linux·运维
阿龙的工作记录6 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
代码村新手7 小时前
Linux的基本指令
linux·运维·服务器
hkj88087 小时前
Ubuntu 切换java版本
java·linux·ubuntu
撩得Android一次心动9 小时前
Linux编程笔记3【个人用】
linux·笔记