进程创建
1.1 fork函数初识
1.2 fork函数返回值
1.3 写时拷贝
1.4 fork常规用法
1.5 fork调用失败的原因
进程终止
2.1 进程退出场景
2.2 进程退出码
2.3 进程正常退出
2.3.1 return退出
2.3.2 exit函数
2.3.3 _exit函数
2.3.4 return、exit和_exit之间的区别与联系
2.4 进程异常退出
进程等待
3.1 进程等待的必要性
3.2 获取子进程status
3.3 进程等待的方法
3.3.1 wait方法
3.3.2 waitpid方法
3.3.3 多进程创建以及等待的代码模型
3.3.4 基于非阻塞接口的轮询检测方案
进程程序替换
4.1 替换原理
4.2 替换函数
4.3 函数解释
4.4 命名理解
4.5 做一个简易的shell
在 Linux 系统中,fork
函数是进程创建的核心工具。它允许一个现有进程(父进程)创建一个新的进程(子进程)。这个新进程几乎完全复制了父进程的状态,包括代码段、数据段、堆栈以及打开的文件描述符等。
fork
的基本行为当调用 fork
函数时,系统会创建一个新的进程。新进程(子进程)几乎完全复制了父进程的地址空间,包括代码、数据、堆栈等。子进程从 fork
函数的返回点开始执行,但它的返回值是 0
,而父进程的返回值是新创建的子进程的 PID(进程标识符)。
#include
#include
#include
int main() {
pid_t pid = fork(); // 创建子进程
if (pid < 0) {
// 创建失败
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
printf("子进程: 我的PID是 %d\n", getpid());
} else {
// 父进程
printf("父进程: 子进程的PID是 %d\n", pid);
}
return 0;
}
运行这段代码时,父进程和子进程都会继续执行 fork
之后的代码。这意味着,printf
语句会被执行两次:一次由父进程执行,另一次由子进程执行。
fork
的返回值fork
函数的返回值是理解其行为的关键。父进程返回子进程的 PID,而子进程返回 0
。这种设计使得父进程能够知道子进程的 PID,从而可以对其进行管理(如等待子进程结束),而子进程则知道自己是新创建的进程。
fork
当 fork
被调用时,内核会执行以下操作:
fork
返回两次,一次返回子进程的 PID 给父进程,一次返回 0
给子进程。fork
创建子进程后,父子进程的执行顺序由调度器决定。也就是说,父进程和子进程谁先执行是不确定的。
#include
#include
#include
int main() {
printf("Before fork\n"); // 父进程执行
pid_t pid = fork(); // 创建子进程
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
printf("子进程执行\n");
} else {
// 父进程
printf("父进程执行\n");
}
return 0;
}
在这个例子中,"Before fork"
只会被打印一次,因为这是在 fork
之前执行的。而 "子进程执行"
和 "父进程执行"
的顺序是不确定的,这取决于调度器的决策。
fork
函数的返回值机制是进程创建的核心之一。为什么 fork
会返回两次?为什么子进程返回 0
,而父进程返回子进程的 PID?
fork
返回两次?fork
函数的返回值机制源于进程创建的过程。当 fork
被调用时,父进程会执行一系列操作来创建子进程,包括分配内存、复制数据结构等。当子进程创建完成后,操作系统需要让父进程和子进程都能继续执行。因此,fork
函数在父进程和子进程中各返回一次,以表明它们各自的身份。
0
?子进程返回 0
的原因是,子进程不需要知道自己的 PID,因为它可以通过 getpid()
函数获取自己的 PID。而父进程需要知道子进程的 PID,以便对其进行管理。
父进程返回子进程的 PID 是为了能够对子进程进行进一步的控制,比如等待子进程结束(通过 wait
或 waitpid
)、发送信号等。一个父进程可能创建多个子进程,因此父进程必须能够区分这些子进程。
#include
#include
#include
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
printf("子进程: 我的PID是 %d\n", getpid());
} else {
printf("父进程: 子进程的PID是 %d\n", pid);
}
return 0;
}
在这个例子中,父进程和子进程都会打印各自的 PID,但子进程的 fork
返回值是 0
,而父进程的返回值是子进程的 PID。
fork
函数的一个重要特性是写时拷贝(Copy-on-Write)。这个机制优化了内存使用,提高了进程创建的效率。
写时拷贝是一种延迟复制技术。当 fork
创建子进程时,子进程和父进程共享相同的物理内存页,包括代码段、数据段和堆栈。只有当其中一个进程尝试修改共享内存时,操作系统才会复制相应的内存页,以确保两个进程的独立性。
进程具有独立性,不能让子进程的修改影响父进程。然而,如果在创建子进程时立即复制所有内存页,会浪费大量资源。写时拷贝技术通过延迟复制,提高了资源利用率。
#include
#include
#include
int global_var = 100; // 全局变量
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
global_var = 200; // 修改全局变量
printf("子进程: global_var = %d\n", global_var);
} else {
// 父进程
sleep(1); // 等待子进程修改
printf("父进程: global_var = %d\n", global_var);
}
return 0;
}
在这个例子中,子进程修改了全局变量 global_var
,但父进程看到的仍然是原始值,这是因为写时拷贝机制的存在。
fork
函数通常用于创建新进程,以便执行不同的任务。常见的用法包括:
服务器程序常常使用 fork
创建子进程来处理客户端请求。例如,当服务器接受一个客户端连接时,它会创建一个子进程来处理该连接,而父进程继续监听新的连接。
子进程通常会调用 exec
系列函数来执行新的程序。例如,一个 shell 程序会使用 fork
创建子进程,然后调用 exec
执行用户输入的命令。
fork
函数调用失败可能有以下几种原因:
如果系统中已经有太多进程,或者可用内存不足,fork
调用可能会失败。
每个用户在系统中可以创建的进程数是有限的。如果当前用户已经达到了这个限制,fork
调用也会失败。
#include
#include
#include
int main() {
int i = 0;
while (1) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
break;
}
if (pid == 0) {
// 子进程直接退出
return 0;
}
i++;
printf("已创建 %d 个子进程\n", i);
}
return 0;
}
这个程序会不断创建子进程,直到系统资源耗尽或达到进程数限制为止。
进程的退出场景主要有以下三种:
进程退出时会返回一个退出码(exit code),用于指示程序的执行结果。退出码是一个整数,通常 0
表示成功,非零值表示错误。
退出码可以帮助调用者判断程序的执行结果。例如,一个脚本可以通过检查退出码来决定下一步操作。
#include
#include
int main() {
int result = 0;
// 模拟一个错误
if (result != 100) {
printf("程序执行失败\n");
exit(1); // 退出码为1,表示错误
}
return 0;
}
在这个例子中,如果 result
不等于 100
,程序会通过 exit(1)
终止,并返回退出码 1
,表示执行失败。
在 main
函数中使用 return
是最常见的进程退出方式。return
返回的值就是进程的退出码。
exit
函数可以在程序的任何位置调用,它会终止进程,并执行一些清理操作,如关闭文件、释放内存等。
_exit
函数与 exit
类似,但它不会执行清理操作,而是直接终止进程。
函数 | 行为 | 适用场景 |
---|---|---|
return |
从 main 函数返回 |
算法结束 |
exit |
终止进程并执行清理操作 | 正常退出 |
_exit |
直接终止进程,不执行清理操作 | 异常退出 |
进程异常退出通常是因为程序发生了严重错误,如段错误、除零错误等。
#include
#include
int main() {
int a = 10;
int b = 0;
int c = a / b; // 除零错误
return 0;
}
这个程序会因为除零错误而崩溃,操作系统会发送 SIGFPE
信号给进程,导致其异常终止。
当父进程创建子进程后,子进程的生命周期可能比父进程短,也可能长。父进程通常需要等待子进程结束,以获取其退出状态。
wait
或 waitpid
获取子进程的退出码,从而判断子进程的执行结果。子进程的退出状态可以通过 wait
或 waitpid
函数获取。这两个函数的原型如下:
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
status
参数用于存储子进程的退出状态。
wait
函数会阻塞父进程,直到任意一个子进程结束。
#include
#include
#include
#include
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
printf("子进程执行完毕\n");
exit(0);
} else {
// 父进程等待子进程
int status;
wait(&status); // 阻塞等待子进程结束
printf("子进程退出码: %d\n", WEXITSTATUS(status));
}
return 0;
}
在这个例子中,父进程调用 wait
阻塞等待子进程结束,并通过 WEXITSTATUS
获取子进程的退出码。
waitpid
函数提供了更灵活的等待机制,可以指定等待特定的子进程。
#include
#include
#include
#include
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
printf("子进程执行完毕\n");
exit(0);
} else {
// 父进程等待子进程
int status;
pid_t result = waitpid(pid, &status, 0); // 等待特定子进程
if (result == pid) {
printf("子进程退出码: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
在这个例子中,父进程调用 waitpid
明确等待特定的子进程结束。
#include
#include
#include
#include
int main() {
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
printf("子进程 %d 执行完毕\n", getpid());
exit(i); // 返回子进程编号作为退出码
}
}
// 父进程等待所有子进程
int status;
for (int i = 0; i < 3; i++) {
pid_t pid = wait(&status);
printf("子进程 %d 退出码: %d\n", pid, WEXITSTATUS(status));
}
return 0;
}
这个程序创建了三个子进程,并通过 wait
等待所有子进程结束。
waitpid
函数可以通过设置 WNOHANG
标志实现非阻塞等待。
#include
#include
#include
#include
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
sleep(3); // 模拟长时间运行
printf("子进程执行完毕\n");
exit(0);
} else {
// 父进程非阻塞等待
int status;
while (1) {
pid_t result = waitpid(pid, &status, WNOHANG);
if (result == 0) {
printf("子进程尚未结束,继续轮询\n");
sleep(1); // 等待1秒
} else if (result == pid) {
printf("子进程退出码: %d\n", WEXITSTATUS(status));
break;
} else {
perror("waitpid");
break;
}
}
}
return 0;
}
在这个例子中,父进程使用 waitpid
和 WNOHANG
标志实现非阻塞等待,避免了长时间的阻塞。
进程程序替换是指用新程序替换当前进程的地址空间。替换后,原来的进程代码和数据会被新程序的代码和数据替换,但进程的 PID 保持不变。
exec
系列函数用于进程程序替换。常见的函数包括:
execl
execv
execle
execve
execlp
execvp
execl
int execl(const char *path, const char *arg, ...);
execl
用于执行新程序,参数以 NULL
结尾。
execv
int execv(const char *path, char *const argv[]);
execv
的参数是一个指针数组,数组以 NULL
结束。
execle
int execle(const char *path, const char *arg, ..., char *const envp[]);
execle
允许传递环境变量。
execve
int execve(const char *filename, char *const argv[], char *const envp[]);
execve
是 exec
系列函数的底层实现。
execlp
int execlp(const char *file, const char *arg, ...);
execlp
会在 PATH
环境变量中查找可执行文件。
execvp
int execvp(const char *file, char *const argv[]);
execvp
是 execv
的变体,支持 PATH
查找。
exec
系列函数的命名规则如下:
l
:参数以列表形式传递。v
:参数以指针数组形式传递。p
:支持 PATH
查找。e
:允许传递环境变量。一个 shell 程序的基本工作流程如下:
fork
创建子进程。exec
系列函数执行用户输入的命令。wait
或 waitpid
等待子进程结束。#include
#include
#include
#include
#include
#include
#define MAX_CMD 1024
#define MAX_ARGS 64
int main() {
char cmd[MAX_CMD]; // 存储用户输入的命令
while (1) {
// 1. 获取命令行
printf("myshell> ");
fflush(stdout);
if (!fgets(cmd, MAX_CMD, stdin)) {
printf("\n");
break;
}
cmd[strlen(cmd) - 1] = '\0'; // 去掉换行符
// 2. 解析命令
char *args[MAX_ARGS];
int i = 0;
args[i++] = strtok(cmd, " ");
while ((args[i++] = strtok(NULL, " ")) != NULL);
// 3. 创建子进程
pid_t pid = fork();
if (pid < 0) {
perror("fork");
continue;
} else if (pid == 0) {
// 子进程执行命令
execvp(args[0], args);
perror("execvp"); // 如果execvp返回,说明执行失败
exit(1);
} else {
// 父进程等待子进程
int status;
waitpid(pid, &status, 0);
printf("子进程退出码: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
这个简单的 shell 程序实现了命令行解析、子进程创建、命令执行和等待子进程结束的功能。