目录
execle("./mytest", "mytest", NULL, environ); 第二个参数不需要 ./?
进程创建
fork()
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include<unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度
#include <unistd.h>
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;
}
解释:
父进程打印自己的 PID 和子进程的 PID。
子进程打印自己的 PID 和
fork()
的返回值0
。fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器 决定
#include<iostream>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define N 5
void runChild()
{
int cnt = 10;
while(cnt)
{
printf("I am child: %d, ppid:%d\n", getpid(), getppid());
sleep(1);
cnt--;
}
}
int main()
{
for(int i = 0; i < N; i++)
{
pid_t id = fork();
if(id == 0)
{
runChild();
exit(0);
}
}
sleep(1000);
return 0; // 为什么main函数总是会返回return 0? 1? 2?, 这个东西给谁了?为什么要返回这个值?
//printf("pid: %d before!\n", getpid());
//fork();
//printf("pid: %d after!\n", getpid());
}
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副 本。具体见下图:
写时拷贝的实现机制
页表与物理内存
进程的虚拟内存通过 页表(Page Table) 映射到物理内存。
fork()
后,父子进程的页表指向相同的物理页,但所有页被标记为 只读(Read-Only)。
触发写时拷贝
当某个进程(父或子)尝试写入共享内存时:
CPU 触发页错误(Page Fault),因为页是只读的。
内核捕获该错误,检查是否是由于 COW 触发的。
内核分配新的物理页,复制原页内容,并更新当前进程的页表,使其指向新页。
重新执行写入指令,此时进程操作的是自己的私有副本。
共享与私有内存
未修改的页:继续共享,节省内存。
修改后的页:独立副本,互不影响。
fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
系统中有太多的进程
实际用户的进程数超过了限制
进程终止
进程退出场景
代码运行完毕,结果正确
代码运行完毕,结果不正确 代码异常终止
退出码
退出码是进程结束时返回给操作系统的一个整数值,用于表示进程的执行状态。在 Linux/Unix 系统中,0 表示成功(return 0;),非 0 表示错误(不同的非 0 值可以表示不同的错误类型)。
常见退出码
虽然 0
表示成功,但 非 0 值的含义由程序自行定义。以下是 常见的约定(非强制标准):
退出码 | 含义 | 常见用途 |
---|---|---|
0 | 成功 | 程序正常结束 |
1 | 通用错误 | catch-all 错误 |
2 | 命令行参数错误 | bash 、grep 使用 |
126 | 不可执行 | 权限不足 |
127 | 命令未找到 | Shell 命令不存在 |
128 | 无效退出码 | 不能直接使用 |
130 | 被 SIGINT 终止(Ctrl+C) | 用户中断 |
137 | 被 SIGKILL 终止(kill -9 ) | 强制杀死 |
255 | 超出范围(只取低 8 位) | 例如 exit -1 返回 255 |
查看退出码:
1. echo $? $?存储上一个命令的退出码
2.c语言方式获得
#include <sys/wait.h> #include <unistd.h> #include <stdio.h> int main() { pid_t pid = fork(); if (pid == 0) { // 子进程 exit(123); // 子进程返回 123 } else { // 父进程 int status; wait(&status); // 等待子进程结束 if (WIFEXITED(status)) { printf("Child exited with: %d\n", WEXITSTATUS(status)); // 输出 123 } } return 0; }
WIFEXITED(status)
:判断是否正常退出。
WEXITSTATUS(status)
:获取退出码。
进程常见退出方法
异常退出:
ctrl + c,信号终止
进程因错误或外部干预而被迫终止。
如
kill
信号(kill -l查看)
错误码
errno存最后一个错误码
打印错误码
1 #include <stdio.h> 2 #include <cerrno> 3 #include <cstring> 4 5 int main() 6 { 7 8 for(int i = 0; i < 200; i++) 9 { 10 printf("%d: %s\n", i, strerror(i)); 11 } 12 13 return 0; 14 } 15
正常终止:
进程主动调用退出函数或从 main()
返回,属于预期内的终止。
从
main()
返回
return 0
表示成功退出(EXIT_SUCCESS
)。非 0 值(如
return 1
)通常表示错误(EXIT_FAILURE
)。
函数 | 所属库 | 行为 | 适用场景 |
---|---|---|---|
exit() | <stdlib.h> | 执行清理(刷新缓冲区、调用 atexit 等)后退出 | 主进程正常退出 |
_exit() | <unistd.h> | 直接终止进程,不执行任何清理 | 子进程退出,避免干扰父进程 |
调用 _exit()
/ _Exit()(立即终止,推荐用于子进程)
#include <unistd.h> int main() { void _exit(int status);// 立即终止,不清理缓冲区 }
_exit()
是系统调用status 定义了进程的终止状态,父进程通过wait来获取该值不会:刷新
stdio
缓冲区。调用atexit()
注册的函数。主要用于子进程退出,避免干扰父进程。虽然status是int,但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时,在终端执⾏$?发现 返回值是255。
示例代码1:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <iostream> void func() { printf("666"); // 未加‘\n’,C 的 printf,未刷新 std::cout << "666666" << std::endl; // C++ 的 cout,立即刷新 _exit(10); // 直接终止,不刷新 C 缓冲区 } int main() { func(); printf("进程退出了"); // 不会执行,因为 _exit(10) 已终止进程 sleep(2); _exit(20); return 0; }
第一个只有printf,第二个两个都有
"进程结束了"这句话并没有打印,所以得到结论 _exit 方法的作用是结束整个进程。
示例代码2:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 5 int main() 6 { 7 printf("进程运行结束!"); 8 sleep(2); 9 _exit(20); 10 11 return 0; 12 }
_exit()不会帮我们刷新缓冲区
调用 exit()(标准终止,推荐用于主进程)
#include <stdlib.h> void exit(int status); // status 是退出码(0=成功,非0=错误)
exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
1. 执行用户通过 atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit
示例代码1:
#include <iostream> #include <unistd.h> #include <stdlib.h> using namespace std; int func() { cout << "66666"<<endl; exit(1); return 1; } int main() { int funcret = func(); cout << funcret << endl; return 0; }
调用
exit(1)
直接终止程序,返回退出码1
。后面的
return 1
永远不会被执行,因为exit(1)
会导致程序立即退出。main函数调用func函数程序直接exit退出了,不会执行后边代码
exit 方法的作用是结束整个进程
示例代码2:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 5 int main() 6 { 7 printf("进程运行结束!"); 8 sleep(2); 9 exit(20); 10 11 return 0; 12 }
如果不加‘\n’的话exit 方法结束进程前会主动帮我们刷新缓冲区。(行缓冲机制)
return退出
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返 回值当做 exit的参数。
以下是Linux中return
、exit()
、_exit()
的区别总结表格:
特性 | return | exit() | _exit() |
---|---|---|---|
所属范畴 | C语言关键字 | C标准库函数(stdlib.h) | 系统调用(unistd.h) |
作用对象 | 函数退出 | 进程退出 | 进程立即退出 |
清理操作 | 自动清理局部变量 | 调用atexit()注册的函数,刷新I/O缓冲区 | 直接终止进程,不刷新I/O缓冲区 |
头文件 | 无需 | #include <stdlib.h> | #include <unistd.h> |
参数类型 | 返回函数的值(任意类型) | int 状态码(通常0表示成功) | int 状态码(通常0表示成功) |
使用场景 | 函数内部退出 | 程序正常退出,需清理资源 | 紧急退出或子进程退出时避免重复清理 |
缓冲区处理 | 无直接影响 | 刷新stdin/stdout/stderr 缓冲区 | 不刷新缓冲区(可能导致数据丢失) |
示例 | return 0; (函数内) | exit(0); (任意位置) | _exit(1); (立即终止) |
进程等待
进程等待是系统调用wait/waitpid,来对子进程进行状态检测与回收的功能
进程等待必要性
1.子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
2.僵尸进程无法被杀死,kill -9 也无能为力,需要通过进程等待杀掉他,解决内存泄漏问题·。
3.父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的作用
回收子进程资源:避免僵尸进程(子进程终止但未被父进程回收)。
获取子进程退出状态:判断子进程是正常退出(
exit
)、被信号终止(signal
)还是异常终止。同步父子进程:父进程可以等待子进程完成任务后再继续执行。
进程等待的方法
Linux 提供了两个主要的系统调用用于进程等待:
系统调用 | 描述 | 头文件 |
---|---|---|
wait() | 阻塞等待任意一个子进程终止,返回子进程 PID,并获取退出状态。 | <sys/wait.h> |
waitpid() | 可以指定等待某个子进程,支持非阻塞模式(WNOHANG )。 | <sys/wait.h> |
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
依次等待任意一个子进程,调wait时子进程不退出父进程一直等待(阻塞等待)
等待成功ret就是子进程PID,给父进程返回子进程PID判断父进程等待哪个子进程,给子进程ID返回0,ret<0等待失败
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULLwait(null)表示父进程不关心子进程的退出状态,仅等待子进程终止并回收其资源(避免僵尸进程)。
示例代码
1 #include<iostream>
2 #include<errno.h>
3 #include<stdio.h>
4 #include<unistd.h>
5 #include <sys/types.h>
6 #include <sys/wait.h>
7 #include<stdlib.h>
8 #include<string.h>
9 #define N 5
10 //void runChild()
11 //{
12 // int cnt = 10;
13 // while(cnt)
14 // {
15 // printf("I am child: %d, ppid:%d\n", getpid(), getppid());
16 // sleep(1);
17 // cnt--;
18 // }
19 //}
20
21 int main()
22 {
23 pid_t id = fork();
// if(id < 0)
//{
// printf("errno: %d,errstring: %s\n",errno,strerror(errno));
// return errno;
// }
25 if(id==0)
26 {
27 //child
28 int cnt = 3;
29 while(cnt--)
30 {
31 printf("子进程:pid:%d\n",getpid());
32 sleep(1);
33 }
34 exit(123);
35 }
36 else{
37 sleep(2);
38 pid_t rid = wait(NULL);
39 if(rid>0)
40 {
41 printf("等待成功,rid:%d\n",rid);
42 }
43 else{
44 perror("wait fail:");
45 }
46 while(true)
47 {
48 printf("我是父进程,pid:%d\n",getpid());
49 sleep(1);
50 }
51 }
waitpid方法
参数说明
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid=0,等待与调用进程同进程组的任意子进程
Pid>0.等待其进程ID与pid相等的子进程。
status:
用于存储子进程的退出状态,可以通过以下宏解析:
WIFEXITED(status)
:若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status)
:若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
WIFSIGNALED(status)
:子进程是否被信号终止
WTERMSIG(status)
:获取终止子进程的信号编号
WIFSTOPPED(status)
:子进程是否被停止
WSTOPSIG(status)
:获取停止子进程的信号编号null:不关心子进程的退出状态,只等待子进程终止并回收其资源。
options:
0:阻塞模式
WNOHANG(非阻塞模式): 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进 程的ID。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退 出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
示例代码
1 #include<iostream>
2 #include<errno.h>
3 #include<stdio.h>
4 #include<unistd.h>
5 #include <sys/types.h>
6 #include <sys/wait.h>
7 #include<stdlib.h>
8 #include<string.h>
9 #define N 5
21 int main()
22 {
23 pid_t id = fork();
24
25 if(id==0)
26 {
27 //child
28 int cnt = 3;
29 while(cnt--)
30 {
31 printf("子进程:pid:%d\n",getpid());
32 sleep(1);
33 }
34 exit(123);
35 }
36 else{
37 sleep(2);
38 int status = 0;
39 pid_t rid = waitpid(id,&status,0);//操作系统发现子进程退出将返回值给到status
40 if(rid>0)
41 {
42 printf("等待成功,rid:%d\n",rid);
43 }
44 else{
45 perror("wait fail:");
46 }
47 while(true)
48 {
49 printf("我是父进程,pid:%d\n",getpid());
50 sleep(1);
51 }
52 }
53 return 0;
54 }
STATUS
介绍
waitpid
和wait
函数中的status
参数是一个非常重要的整型变量,它存储了子进程的退出状态信息。wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特 位):
次低八位:代表退出状态
低八位:是否出现异常问题(低八位显示是否core dump)
低七位是0就没有收到信号(kill没有0编号),程序跑完了
status
的基本作用
父子进程具有独立性,用全局变量获取子进程status,传给父进程不可行,必须用系统调用
status
是一个整型指针参数,用于接收子进程的退出状态信息。通过解析这个值,父进程可以知道:
子进程是正常退出还是被信号终止
子进程的退出码(如果是正常退出)
导致子进程终止的信号编号(如果是被信号终止)
示例代码
1 #include<iostream>
2 #include<errno.h>
3 #include<stdio.h>
4 #include<unistd.h>
5 #include <sys/types.h>
6 #include <sys/wait.h>
7 #include<stdlib.h>
8 #include<string.h>
9 #define N 5
21 int main()
22 {
23 pid_t id = fork();
24
25 if(id==0)
26 {
27 //child
28 int cnt = 3;
29 while(cnt--)
30 {
31 printf("子进程:pid:%d\n",getpid());
32 sleep(1);
33 }
34 exit(123);
35 }
36 else{
37 sleep(2);
38 int status = 0;
39 pid_t rid = waitpid(id,&status,0);
40 if(rid>0)
41 {
42 if(WIFEXITED(status))
43 {
44 printf("等待成功,rid:%d,status code:%d,exit signal:%d\n",rid,(status>>8)&0xFF,status&0x7F);
45 printf("等待成功,rid:%d,status code:%d\n",rid,WEXITSTATUS(status));
46 }
47 else{
48 printf("子进程退出失败");
49 }
50 }
51 else{
52 perror("wait fail:");
53 }
54 while(true)
55 {
56 printf("我是父进程,pid:%d\n",getpid());
57 sleep(1);
58 }
59 }
60 return 0;
61 }
阻塞与非阻塞等待
进程的阻塞等待方式(文件方式,后边博客会有文件操作):
#include <iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctime>
#include <string>
enum {
OK = 0,
OPEN_FILE_ERROR,
FORK_ERROR
};
const std::string gsep = " ";
std::vector<int> data;
// 备份数据到文件
int SaveBegin()
{
// 生成带时间戳的备份文件名
std::string name = std::to_string(time(nullptr)) + ".backup";
// 打开文件
FILE *fp = fopen(name.c_str(), "w");
if(fp == nullptr) {
perror("fopen failed");
return OPEN_FILE_ERROR;
}
// 准备要写入的数据
std::string dataStr;
for (auto d : data) {
dataStr += std::to_string(d) + gsep;
}
// 写入数据并关闭文件
if(fputs(dataStr.c_str(), fp) == EOF) {
perror("fputs failed");
fclose(fp);
return OPEN_FILE_ERROR;
}
fclose(fp);
return OK;
}
// 使用阻塞等待方式执行备份
void Save()
{
pid_t id = fork();
if(id < 0) {
perror("fork failed");
return;
}
if(id == 0) { // 子进程
int code = SaveBegin();
exit(code); // 子进程退出,返回备份结果
}
// 父进程 - 阻塞等待子进程结束
int status = 0;
pid_t rid = waitpid(id, &status, 0); // 0表示阻塞等待
if(rid > 0) {
if(WIFEXITED(status)) { // 子进程正常退出
int code = WEXITSTATUS(status);
if(code == OK) {
printf("备份成功,文件名: %ld.backup\n", time(nullptr));
} else {
printf("备份失败,错误码: %d\n", code);
}
} else if(WIFSIGNALED(status)) { // 子进程被信号终止
printf("备份进程被信号终止,信号编号: %d\n", WTERMSIG(status));
}
} else {
perror("waitpid failed");
}
}
int main()
{
std::cout << "数据备份程序启动 (每10秒自动备份一次)\n";
int cnt = 1;
while(true) {
data.push_back(cnt++);
sleep(1);
if(cnt % 10 == 0) {
printf("\n准备执行第%d次备份...\n", cnt/10);
Save(); // 调用阻塞式备份函数
}
}
return 0;
}
进程的非阻塞等待模式
非阻塞轮询:
父进程不会因为等待子进程而被阻塞
可以同时处理其他任务
能及时响应子进程的退出
等待时子进程没退也返回0
主要是周期性等待,做自己任务是捎带,要轻量化
进程等待保证父进程最后一个退出,可以回收子进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
// 定义最大任务数量
#define TASK_NUM 10
// 定义任务函数指针类型
typedef void(*task_t)();
// 任务数组,存储所有注册的任务函数
task_t tasks[TASK_NUM];
// 任务1:打印日志
void task1() {
printf("打印日志任务, pid: %d\n", getpid());
}
// 任务2:检测网络状态
void task2() {
printf("检测网络状态任务, pid: %d\n", getpid());
}
// 任务3:绘制图形界面
void task3() {
printf("绘制图形界面任务, pid: %d\n", getpid());
}
// 初始化任务系统
void InitTask() {
// 清空任务数组
for(int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;
// 注册三个示例任务
tasks[0] = task1;
tasks[1] = task2;
tasks[2] = task3;
}
// 执行所有已注册的任务
void ExecuteTask() {
for(int i = 0; i < TASK_NUM; i++) {
if(tasks[i]) { // 检查任务是否有效
tasks[i](); // 执行任务
}
}
}
// 主函数:演示非阻塞等待子进程
int main() {
// 创建子进程
pid_t id = fork();
if(id < 0) {
// fork失败处理
perror("fork");
return 1;
}
else if(id == 0) {
// 子进程代码
int cnt = 5;
while(cnt) {
printf("子进程运行中... pid:%d, 剩余:%d秒\n", getpid(), cnt);
cnt--;
sleep(1); // 每秒打印一次
}
exit(11); // 子进程退出,返回状态码11
}
else {
// 父进程代码 - 非阻塞等待
// 初始化任务系统
InitTask();
int status = 0; // 用于存储子进程状态
// 轮询循环
while(1) {
// 非阻塞等待子进程(WNOHANG选项使waitpid立即返回)
pid_t ret = waitpid(id, &status, WNOHANG);
if(ret > 0) {
// 子进程已退出
if(WIFEXITED(status)) {
printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
} else {
printf("子进程异常终止\n");
}
break; // 退出轮询循环
}
else if(ret < 0) {
// 等待出错
printf("等待失败\n");
break;
}
else {
// ret == 0 表示子进程仍在运行
// 执行其他任务(非阻塞)
ExecuteTask();
usleep(500000); // 休眠0.5秒(避免CPU占用过高)
}
}
}
return 0;
}
两种方式的对比
特性 | 阻塞等待 | 非阻塞等待 |
---|---|---|
父进程行为 | 挂起等待 | 继续执行 |
实现方式 | wait() 或 waitpid(..., 0) | waitpid(..., WNOHANG) |
资源占用 | 父进程不消耗CPU | 需要轮询检查 |
适用场景 | 简单顺序执行 | 需要并发处理 |
响应速度 | 子进程结束立即响应 | 需要主动轮询 |
多子进程的等待 (阻塞)
父进程 创建 10 个子进程(
N=10
)。每个子进程 执行
RunChild()
函数,打印 5 次信息后退出。父进程 等待所有子进程结束,并打印它们的退出状态。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define N 10
void RunChild()
{
int cnt = 5;
while(cnt)
{
printf("I am Child Process, pid: %d, ppid:%d\n", getpid(), getppid());
sleep(1);
cnt--;
}
}
int main()
{
//wait / waitpid
for(int i = 0; i < N; i++)
{
pid_t id = fork();
if(id == 0)
{
RunChild();
exit(i);
}
printf("create child process: %d success\n", id); // 这句话只有父进程才会执行
}
//sleep(10);
// 等待
for(int i = 0; i < N; i++)
{
// wait当任意一个子进程退出的时候,wait回收子进程
// 如果任意一个子进程都不退出呢?
pid_t id = wait(NULL);
int status = 0;
pid_t pid = waitpid(-1, &status, 0);
if(id > 0)
{
printf("wait %d success, exit code: %d\n", pid, WEXITSTATUS(status));
}
}
return 0;
}
多子进程的等待 (非阻塞)
用数组
child_pids[N]
存储所有子进程PID,便于跟踪。父进程创建
N
个子进程,每个子进程执行RunChild()
。父进程进入轮询循环:调用
waitpid(WNOHANG)
检查子进程状态。如果有子进程退出,打印其信息并减少计数器。
如果子进程仍在运行,父进程执行其他任务(如监控)。
当所有子进程退出后,父进程结束。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define N 10 // 子进程数量
void RunChild(int child_id) {
int cnt = 5;
while (cnt--) {
printf("Child %d (PID:%d) running... (cnt=%d)\n",
child_id, getpid(), cnt);
sleep(1);
}
exit(child_id); // 退出码设为子进程编号
}
int main() {
pid_t child_pids[N]; // 存储所有子进程PID
// 1. 创建N个子进程
for (int i = 0; i < N; i++) {
pid_t pid = fork();
if (pid == 0) {
RunChild(i); // 子进程执行任务
} else if (pid > 0) {
child_pids[i] = pid;
printf("Parent: Created child %d (PID:%d)\n", i, pid);
} else {
perror("fork failed");
exit(1);
}
}
// 2. 父进程非阻塞轮询等待子进程退出
int remaining_children = N;
while (remaining_children > 0) {
int status;
pid_t pid = waitpid(-1, &status, WNOHANG); // 非阻塞模式
if (pid > 0) {
// 有子进程退出
if (WIFEXITED(status)) {
printf("Parent: Child %d exited with code %d\n",
pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Parent: Child %d killed by signal %d\n",
pid, WTERMSIG(status));
}
remaining_children--;
} else if (pid == 0) {
// 子进程仍在运行,父进程可以执行其他任务
printf("Parent: Monitoring... (%d children still running)\n",
remaining_children);
sleep(1); // 模拟父进程执行其他任务
} else {
// waitpid错误
perror("waitpid failed");
break;
}
}
printf("Parent: All children exited.\n");
return 0;
}
进程程序替换
不创建新进程,之进行程序的程序代码和数据的替换工作
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
./运行程序时,操作系统内核帮我们创建PCB、页表,将进程对应的代码加载到内存里(数据也加载),再通过页表映射。
代码可以被写入操作系统
替换函数
exec
系列函数共有 6 种,均定义在 <unistd.h>
中:
函数 | 说明 |
---|---|
execl | 参数list形式传入程序路径和参数(argv[0], argv[1], ..., NULL ) |
execv | 参数vector形式传入程序路径和参数(char *argv[] ) |
execlp | 类似 execl ,但会在 PATH 环境变量中查找可执行文件 |
execvp | 类似 execv ,但会在 PATH 环境变量中查找可执行文件 |
execle | 类似 execl ,但可以指定环境变量(char *envp[] ) |
execve | 系统调用,参数数组形式传入程序路径、参数和环境变量 |
注意:
这些函数 仅在失败时返回(返回
-1
),成功时 原进程的代码段和数据段会被完全替换,但 PID 不变。execl后的代码不能执行了(旧程序代码)已经被替换为新程序
通常与
fork()
结合使用,先创建子进程,再在子进程中调用exec
执行新程序。
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
带p自动在bash中查找,所有子进程集成bash环境变量,环境变量具有全局属性
小知识:
Linux中形成的可执行程序是有格式的,ELF,可执行程序的表头,可执行程序的入口地址就在表中。当调用execve()
时,内核需要解析ELF文件的头部以正确加载程序:
此类方法的标准写法
命令行怎么写将空格换成“,”即可(第二个以后的参数)
execl("/usr/bin/ls","ls","-a","-l",NULL); execlp("ls","ls","-a","-a",NULL);
ls有main函数和命令行参数,其命令行参数由execv系统调用,由myargv函数传入
execv("/usr/bin/ls‘,myargv);将myargv传递给ls的main函数
execv代码级别加载器,将可执行程序从磁盘导到内存
先告诉在哪找再告诉怎么执行
execl底层先转化为
示例代码
makefile
.PHONY:all all:myprocess mytest myprocess:myprocess.c gcc -o $@ $^ mytest:mytest.cc g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f myprocess mytest
myprocess.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { // 定义一个自定义的环境变量数组 char *const env[] = { (char*)"haha=hehe", // 自定义环境变量1 (char*)"PATH=/", // 修改PATH环境变量 NULL // 必须以NULL结尾 }; // 打印当前进程的PID printf("I am a process, pid: %d\n", getpid()); // putenv("MYVAL=bbbbbbbbbbbbbbbbbbbbbbbbbbbb"); // 这行被注释掉的代码可以动态添加环境变量 // 创建子进程 pid_t id = fork(); // 子进程代码块 if(id == 0) { // 声明外部环境变量指针(系统全局变量) extern char** environ; // 子进程睡眠1秒 sleep(1); /******************************/ /* 以下是各种exec函数的用法示例 */ /******************************/ // 1. 使用execle并传递当前环境变量 execle("./mytest", "mytest", NULL, environ); // 问题:我们真的需要传递environ吗?子进程不是已经继承了环境变量吗? // 答案:execle的最后一个参数会完全替换子进程的环境变量表, // 如果不传environ,新程序将没有任何环境变量 // 2. 使用execle并传递自定义环境变量 // execle("./mytest", "mytest", NULL, env); // 这里env会完全替换子进程的环境变量 // 3. 执行Python脚本 // execl("/usr/bin/python3", "python3", "test.py", NULL); // 使用默认环境变量 // 4. 执行Bash脚本 // execl("/usr/bin/bash", "bash", "test.sh", NULL); // 使用默认环境变量 // 5. 最简单的execl调用 // execl("./mytest", "mytest", NULL); // 问题:没有显式传递环境变量,但子进程仍然能获取到 // 这是因为execl会保留当前进程的环境变量 // 6. 使用参数数组的execv方式 // char *const argv[] = { // (char*)"ls", // (char*)"-a", // (char*)"-l", // NULL //}; // sleep(3); // printf("exec begin...\n"); // execvp("ls", argv); // 自动搜索PATH // execv("/usr/bin/ls", argv); // 指定完整路径 // 7. 使用execlp执行ls命令 // execlp("ls", "ls", "-a", "-l", NULL); // execlp会自动在PATH中查找命令 // 8. 执行top命令 // execl("/usr/bin/top", "/usr/bin/top", NULL); // 如果exec函数执行失败,会继续执行下面的代码 printf("exec end ...\n"); exit(1); } // 父进程等待子进程结束 pid_t rid = waitpid(id, NULL, 0); if(rid > 0) { printf("wait success\n"); } exit(1); }
mytest.c
#include <iostream> #include <unistd.h> using namespace std; int main() { for(int i = 0; environ[i]; i++) { printf("env[%d]: %s\n", i, environ[i]); } // for(int i = 0; argv[i]; i++) // { // printf("argv[%d]: %s\n", i, argv[i]); // } // cout << "hello C++" << endl; // cout << "hello C++" << endl; // cout << "hello C++" << endl; // cout << "hello C++" << endl; // cout << "hello C++" << endl; // cout << "hello C++" << endl; // cout << "hello C++" << endl; // cout << "hello C++" << endl; cout << "hello C++" << endl; return 0; }
test.py
#!/usr/bin/python3 print("hello python")
test.sh
#!/usr/bin/bash function myfun() { cnt = 1 while[$cnt -le 10] do echo "hello $cnt" let cnt++ done } echo "hello shell" echo "hello shell" echo "hello shell" echo "hello shell" echo "hello shell" echo "hello shell" echo "hello shell" echo "hello shell" ls -a -l myfun()//函数名当命令看
环境变量也是数据,创建子进程的时候,环境变量就已经被子进程集成下去了
程序替换时,环境变量信息不会被替换
extern char* environ第三方变量,指向环境变量信息(environ定义时已经被父进程初始化,指向自己的环境变量表了)
putenv()添加环境变量(父进程中没有),添加到调用进程的上下文
execle采用的是覆盖而不是追加
putenv修改了环境变量标的指针,让他指向导出的环境变量
添加环境变量不是直接放入数据,而是让空闲指针指向这一块新开辟的空间,若被覆盖就找不到了,环境变量就不见了,getenv就找不到名字了。
环境变量是张表,表中没用字符串,而是字符串的地址
shell本身环境变量是在启动的时候直接从操作系统的配置文件中直接读取来的
execle("./mytest", "mytest", NULL, environ); 第二个参数不需要
./
?它不是文件路径
第二个参数
"mytest"
只是argv[0]
,用于告诉新程序 "你被调用时的名称是什么"。它不会影响程序的加载行为,纯粹是标识用途。
#! 脚本语言开头+脚本语言解释器
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在 man手册第3节。这些函数之间的关系如下图所示。
下图exec函数族 一个完整的例子:
自己写一个简单的shell
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结 束。
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。 所以要写一个shell,需要循环以下过程:
1. 获取命令行
2. 解析命令行
3. 建立一个子进程(fork)
4. 替换子进程(execvp)
5. 父进程等待子进程退出(wait)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
// 定义常量
#define SIZE 1024 // 缓冲区大小
#define MAX_ARGC 64 // 最大参数个数
#define SEP " " // 命令行参数分隔符(空格)
// 全局变量
char *argv[MAX_ARGC]; // 存储解析后的命令行参数
char pwd[SIZE]; // 存储当前工作目录(用于环境变量PWD)
char env[SIZE]; // 存储环境变量(用于export命令)
int lastcode = 0; // 存储上一条命令的退出状态码
// 获取主机名
const char* HostName()
{
char *hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "None";
}
// 获取用户名
const char* UserName()
{
char *hostname = getenv("USER");
if(hostname) return hostname;
else return "None";
}
// 获取当前工作目录
const char *CurrentWorkDir()
{
char *hostname = getenv("PWD");
if(hostname) return hostname;
else return "None";
}
// 获取家目录路径
char *Home()
{
return getenv("HOME");
}
// 交互函数:显示提示符并获取用户输入
int Interactive(char out[], int size)
{
// 显示提示符:[用户名@主机名 当前目录]$
printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
// 获取用户输入
fgets(out, size, stdin);
// 去除末尾的换行符
out[strlen(out)-1] = 0;
// 返回输入字符串长度(0表示空输入)
return strlen(out);
}
// 分割命令行字符串为参数数组
void Split(char in[])
{
int i = 0;
// 使用strtok分割字符串
argv[i++] = strtok(in, SEP); // 第一个参数(命令名)
// 继续分割剩余参数
while(argv[i++] = strtok(NULL, SEP)); // 注意这里是赋值而非比较
// 特殊处理ls命令,添加--color参数
if(strcmp(argv[0], "ls") ==0)
{
argv[i-1] = (char*)"--color";//strtok 循环结束时,i 指向的是 参数列表的终止符 NULL 的位置。
argv[i] = NULL;
}
}
// 执行外部命令
void Execute()
{
// 创建子进程
pid_t id = fork();
if(id == 0) // 子进程
{
// 执行命令
execvp(argv[0], argv);
// 如果execvp失败,子进程退出
exit(1);
}
// 父进程等待子进程结束
int status = 0;
pid_t rid = waitpid(id, &status, 0);
// 记录子进程退出状态
if(rid == id) lastcode = WEXITSTATUS(status);
}
// 处理内建命令
int BuildinCmd()
{
int ret = 0; // 返回值:1表示是内建命令,0表示不是
// 检查是否是cd命令
if(strcmp("cd", argv[0]) == 0)
{
ret = 1;
char *target = argv[1]; // 获取目标目录
// 如果没有参数,默认切换到HOME目录
if(!target) target = Home();
// 切换目录
chdir(target);
// 更新PWD环境变量
char temp[1024];
getcwd(temp, 1024); // 获取当前工作目录
snprintf(pwd, SIZE, "PWD=%s", temp);
putenv(pwd);
}
// 检查是否是export命令
else if(strcmp("export", argv[0]) == 0)
{
ret = 1;
if(argv[1]) // 如果有参数
{
// 设置环境变量
strcpy(env, argv[1]);
putenv(env);
}
}
// 检查是否是echo命令
else if(strcmp("echo", argv[0]) == 0)
{
ret = 1;
if(argv[1] == NULL) {
// 没有参数,输出空行
printf("\n");
}
else{
// 处理$开头的变量
if(argv[1][0] == '$')
{
// 处理$? (上一条命令的退出状态)
if(argv[1][1] == '?')
{
printf("%d\n", lastcode);
lastcode = 0;
}
else{
// 获取环境变量值
char *e = getenv(argv[1]+1);
if(e) printf("%s\n", e);
}
}
else{
// 直接输出参数
printf("%s\n", argv[1]);
}
}
}
return ret;
}
// 主函数
int main()
{
// 主循环
while(1)
{
char commandline[SIZE];
// 1. 获取用户输入
int n = Interactive(commandline, SIZE);
if(n == 0) continue; // 空输入则跳过
// 2. 解析命令行参数
Split(commandline);
// 3. 处理内建命令
n = BuildinCmd();
if(n) continue; // 如果是内建命令则跳过执行外部命令
// 4. 执行外部命令
Execute();
}
return 0;
}
思考函数和进程之间的相似性
exec/exit就像call/return 一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的 操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。 这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程 序之内的模式扩展到程序之间。如下图
一一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来 返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。