【Linux】进程控制
本文主题内容
本文讲解 Linux 进程控制,主要内容包括:
- 进程创建,掌握 fork 和 vfork
- 进程终止,认识 $? 和退出码
- 进程等待
- 进程程序替换
- 通过微型 shell 重新认识 shell 的运行原理
Linux中很多看似简单的操作,背后都离不开进程控制。一个命令如何被执行,子进程如何创建,程序如何替换,退出状态如何返回给父进程,这些内容是系统编程的基础,也是学习 shell、服务端开发和并发编程绕不开的部分。本文从最基础的 fork 讲起,逐步把进程创建、终止、等待、替换和 shell 执行流程串成一条线,帮助把零散知识点连成完整体系。
一、进程创建
1.1 fork函数初识
在 Linux 中,fork 是非常重要的系统调用。它会从一个已经存在的进程中创建一个新进程。新创建的进程叫子进程,原来的进程叫父进程。
函数原型如下:
#include <unistd.h>
pid_t fork(void);
fork 的返回值规则很重要:
- 子进程中返回 0
- 父进程中返回子进程的进程号
- 出错返回 -1
这里最有问题的是:为什么一个 fork 会有两个返回值。原因很简单,fork 调用后,父子进程都会从 fork 之后的位置继续执行,只不过它们的返回值不同,内核会根据当前执行的是父进程还是子进程分别设置返回值。
进程调用 fork 后,内核会做几件事:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝到子进程
- 把子进程加入系统进程列表
- fork 返回后,由调度器决定谁先运行


1.2 fork函数返回值
fork 的返回值之所以设计成父进程返回子进程 PID、子进程返回 0,是为了让父子进程能够很方便地通过分支语句区分自己的身份。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
printf("Before: pid is %d\n", getpid()); // 获取当前进程的 ID(PID)
if ((pid = fork()) == -1) // 创建失败
perror("fork()"), exit(1); // 报错并返回
printf("After:pid is %d, fork return %d\n", getpid(), pid); // fork() 会返回两次
sleep(1);
return 0;
}
运行后通常会看到三行输出:

父进程先打印 Before,然后 fork 后父子进程分别继续执行。子进程不会重新打印 fork 之前的内容,因为 fork 之后两个进程是从同一个位置继续向下运行,但之前的代码已经执行完了。
fork 之后,谁先执行完全由调度器决定,不能依赖代码顺序强行判断。
fork 之后,父子进程分别拥有独立的执行流。
1.3 写时拷贝
在 fork() 之后,父子进程看起来像复制了一份,但实际并不是简单地把所有数据完整复制一遍。Linux 使用了写时拷贝机制。
写时拷贝的核心思想是:
- 父子进程开始时共享数据页
- 只有当任意一方尝试写入时,系统才真正复制一份新的副本
- 这样可以减少无意义的内存拷贝,提高效率
也就是说,fork 后父子进程的代码通常是共享的,数据在没有写入时也是共享的;一旦写入,就会变成各自独立的数据页。
这就是进程独立性能成立的重要技术基础。

fork 的常规用途
fork 常用于两类场景:
- 父进程复制自己,让父子进程执行不同逻辑
- 子进程在 fork 后调用 exec 执行全新的程序
例如,服务器程序里常见的做法就是:
- 父进程负责接收客户端连接
- 子进程负责处理请求
1.4 fork常规用法
fork 的典型使用方式是先创建子进程,再通过 if 分流。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
if(ret < 0)
{
perror("fork");
return 1;
}
else if(ret == 0)
{
printf("I am child : %d!, ret: %d\n", getpid(), ret);
}
else
{
printf("I am father : %d!, ret: %d\n", getpid(), ret);
}
sleep(1);
return 0;
}
逻辑:
ret < 0:创建失败ret == 0:当前是子进程ret > 0:当前是父进程
注意,fork 之后的父子进程都有自己的执行路径,不能再把它们当成一个进程理解。
1.5 fork调用失败的原因
fork 失败通常有两个主要原因:
- 系统中进程数量太多
- 当前用户的进程数超过系统限制
这在资源紧张或者限制比较严格的环境中比较常见。
二、进程终止
进程终止的本质是释放系统资源。这里的资源不仅仅是进程使用过的内存,还包括进程对应的内核数据结构、代码和数据相关信息。
2.1 进程退出场景
进程退出通常有三种情况:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
2.2 进程常见退出方法
进程的退出方式主要分为两类:
正常终止
可以通过 echo $? 查看进程退出码。
常见正常退出方式有:
- 从 main 返回
- 调用 exit
- 调用 _exit
异常退出
例如:
- ctrl + c
- 信号终止
2.3 退出码
退出码,也叫退出状态,用来告诉父进程或 Shell,程序最后运行得怎么样。
一般来说:
- 退出码 0 表示执行成功
- 非 0 表示执行失败或异常结束
常见退出码含义

Linux Shell 中,很多终止信号对应的退出码可以理解为 128 + 信号编号。
可以使用 strerror 获取某些错误码对应的描述信息。
2.4 _exit函数
函数原型如下:
#include <unistd.h>
void _exit(int status);
参数 status 表示进程终止状态,父进程可以通过 wait 获取它。
status 虽然是 int,但真正被父进程使用的只有低 8 位。
所以 _exit(-1) 后,在 Shell 中看到的 $? 往往是 255。
_exit 的特点是:
- 直接终止进程
- 不做额外清理
- 不刷新标准 I/O 缓冲区
2.5 exit函数
函数原型如下:
#include <unistd.h>
void exit(int status);
exit 最后也会调用 _exit,但在调用 _exit 之前,还会做一些额外工作:
- 执行通过
atexit或on_exit注册的清理函数 - 关闭所有打开的流
- 将缓存数据刷新到外部

exit 和 _exit 的区别
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("hello");
exit(0);
}
运行结果通常会输出:
hello
因为 exit 会刷新缓冲区。
如果改成:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("hello");
_exit(0);
}
那么 hello 可能不会立刻输出,因为 _exit 不会做缓冲区刷新。
注意,如果程序中有标准输出缓存,exit 和 _exit 的表现是不同的。
2.6 return退出
在 main 函数里使用 return n,本质上等价于 exit(n)。
这是因为 main 的运行时环境会把返回值当作 exit 的参数。
所以在普通程序中:
return 0等价于正常结束return 非0表示异常或失败
三、进程等待
3.1 进程等待必要性
进程等待非常重要,原因有三个:
- 子进程退出后,如果父进程不管不顾,会形成僵尸进程
- 僵尸进程无法被
kill -9杀掉,因为它已经死了,只是信息还在 - 父进程需要知道子进程执行结果如何
所以父进程要通过等待机制回收子进程资源,并获取子进程退出信息。
3.2 wait方法
函数原型如下:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
返回值:
- 成功返回被等待进程的 PID
- 失败返回 -1
参数:
status是输出型参数- 如果不关心子进程退出状态,可以传
NULL
wait 的作用是:等待任意一个子进程退出,并回收它。
3.3 waitpid方法
函数原型如下:
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
- 正常返回时,返回收集到的子进程 PID
- 如果设置了
WNOHANG,而当前没有已退出的子进程可收集,则返回 0 - 出错返回 -1,并设置 errno
参数说明:
pid = -1:等待任意子进程pid > 0:等待指定 PID 的子进程status:输出型参数options = 0:阻塞等待WNOHANG:非阻塞等待
常用状态判断宏:
WIFEXITED(status):判断是否正常退出WEXITSTATUS(status):提取退出码

3.4 获取子进程status
status 不能简单当作整型使用,它本质上可以看成一个位图,只研究低 16 位就够了。

根据 status 判断子进程是正常退出还是异常退出:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(void)
{
pid_t pid;
if ((pid = fork()) == -1)
perror("fork"), exit(1);
if (pid == 0)
{
sleep(20);
exit(10);
}
else
{
int st;
int ret = wait(&st);
if (ret > 0 && (st & 0X7F) == 0) // 正常退出
{
printf("child exit code:%d\n", (st >> 8) & 0XFF);
}
else if (ret > 0) // 异常退出
{
printf("sig code : %d\n", st & 0X7F);
}
}
}
测试时:
- 子进程正常退出,会打印退出码
- 子进程被信号杀掉,会打印信号编号
3.5 阻塞与非阻塞等待
阻塞等待
阻塞等待就是父进程一直等,直到子进程退出。
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
if(pid < 0)
{
printf("%s fork error\n", __FUNCTION__);
return 1;
}
else if(pid == 0)
{
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(257);
}
else
{
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
printf("this is test for wait\n");
if(WIFEXITED(status) && ret == pid)
{
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}
else
{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
这里有一个常见现象:exit(257) 最终看到的退出码可能是 1,因为退出状态只保留低 8 位。
非阻塞等待
非阻塞等待的特点是:
- 子进程没退出时,父进程不傻等
- 父进程可以做别的事
- 过一会儿再来检查子进程是否退出
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
typedef void (*handler_t)();
std::vector<handler_t> handlers;
void fun_one()
{
printf("这是一个临时任务1\n");
}
void fun_two()
{
printf("这是一个临时任务2\n");
}
void Load()
{
handlers.push_back(fun_one);
handlers.push_back(fun_two);
}
void handler()
{
if (handlers.empty())
Load();
for (auto iter : handlers)
iter();
}
int main()
{
pid_t pid;
pid = fork();
if (pid < 0)
{
printf("%s fork error\n", __FUNCTION__);
return 1;
}
else if (pid == 0)
{
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(1);
}
else
{
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG);
if (ret == 0)
{
printf("child is running\n");
}
handler();
} while (ret == 0);
if (WIFEXITED(status) && ret == pid)
{
printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
}
else
{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
阻塞等待适合必须等结果的场景,非阻塞等待适合父进程还要继续做其他事情的场景。
四、进程程序替换
fork 之后,父子进程执行的是同一份程序代码。如果子进程想执行一个全新的程序,就要用程序替换。
4.1 替换原理
程序替换是通过特定接口,把磁盘上的一个新程序加载到当前进程的地址空间中。

替换发生后:
- 进程 ID 不变
- 用户空间代码和数据被新程序替换
- 从新程序的启动入口开始执行
也就是说,exec 不会创建新进程,只是让当前进程换了一套程序。
4.2 替换函数
exec 函数族共有六个:
#include <unistd.h>
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[]);
函数说明
这些函数的共同特点是:
- 成功后不返回
- 失败返回 -1
所以 exec 函数只有失败返回值,没有成功返回值。
4.3 命名规律
exec 族函数名字虽然多,但规律很好记:
l (list):参数列表形式v (vector):参数数组形式p(path):会自动搜索 PATHe(env):由用户自己传环境变量

使用示例
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
execlp("ps", "ps", "-ef", NULL);
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
execvp("ps", argv);
execve("/bin/ps", argv, envp);
}
注意:
- 真正的系统调用只有
execve - 其他函数最终都会调用
execve
所以 execve 在 man 手册第 2 节,其余函数通常在第 3 节。

4.4 程序替换和环境变量
exec 的工作和环境变量关系很大。
如果带 p,就会自动根据 PATH 找命令;如果带 e,就说明需要调用者自己提供环境表。
这也是 shell 执行命令时非常关键的一部分。
五、自主Shell命令行解释器
5.1 目标
制作一个 shell 需要能够:
- 处理普通命令
- 处理内建命令
- 帮助理解内建命令、本地变量、环境变量
- 帮助理解 shell 的运行原理
5.2 实现原理
shell 的典型运行流程可以概括成以下步骤:
- 获取命令行
- 解析命令行
- fork 创建子进程
- 子进程 exec 执行命令
- 父进程 wait 等待子进程退出
也就是说,shell 本质上是一个循环:
- 读命令
- 解析命令
- 判断是否是内建命令
- 如果不是,就 fork + exec
- 父进程等待结果

5.3 源码
头文件和全局变量
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
using namespace std;
const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;
//全局的命令行参数表
char *gargv[argvnum];
int gargc = 0;
//全局的变量
int lastcode = 0;
// 我的系统的环境变量
char *genv[envnum];
//全局的当前shell工作路径
char pwd[basesize];
char pwdenv[basesize];
去空白宏
#define TrimSpace(pos) do{\
while(isspace(*pos)){\
pos++;\
}\
}while(0)
获取用户名、主机名和当前路径
string GetUserName()
{
string name = getenv("USER");
return name.empty() ? "None" : name;
}
string GetHostName()
{
string hostname = getenv("HOSTNAME");
return hostname.empty() ? "None" : hostname;
}
string GetPwd()
{
if(nullptr == getcwd(pwd, sizeof(pwd)))
return "None";
snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
putenv(pwdenv);
return pwd;
}
处理当前目录显示
string LastDir()
{
string curr = GetPwd();
if(curr == "/" || curr == "None")
return curr;
size_t pos = curr.rfind("/");
if(pos == std::string::npos)
return curr;
return curr.substr(pos + 1);
}
string MakeCommandLine()
{
char command_line[basesize];
snprintf(command_line, basesize, "[%s@%s %s]# ",
GetUserName().c_str(),
GetHostName().c_str(),
LastDir().c_str());
return command_line;
}
void PrintCommandLine()
{
printf("%s", MakeCommandLine().c_str());
fflush(stdout);
}
获取用户输入
bool GetCommandLine(char command_buffer[], int size)
{
char *result = fgets(command_buffer, size, stdin);
if(!result)
{
return false;
}
command_buffer[strlen(command_buffer) - 1] = 0;
if(strlen(command_buffer) == 0)
return false;
return true;
}
解析命令行
void ParseCommandLine(char command_buffer[], int len)
{
(void)len;
memset(gargv, 0, sizeof(gargv));
gargc = 0;
const char *sep = " ";
gargv[gargc++] = strtok(command_buffer, sep);
while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
gargc--;
}
调试输出
void debug()
{
printf("argc: %d\n", gargc);
for(int i = 0; gargv[i]; i++)
{
printf("argv[%d]: %s\n", i, gargv[i]);
}
}
执行外部命令
bool ExecuteCommand()
{
pid_t id = fork();
if(id < 0)
return false;
if(id == 0)
{
execvpe(gargv[0], gargv, genv);
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
if(WIFEXITED(status))
{
lastcode = WEXITSTATUS(status);
}
else
{
lastcode = 100;
}
return true;
}
return false;
}
添加环境变量
void AddEnv(const char *item)
{
int index = 0;
while(genv[index])
{
index++;
}
genv[index] = (char*)malloc(strlen(item) + 1);
strncpy(genv[index], item, strlen(item) + 1);
genv[++index] = nullptr;
}
处理内建命令
bool CheckAndExecBuiltCommand()
{
if(strcmp(gargv[0], "cd") == 0)
{
if(gargc == 2)
{
chdir(gargv[1]);
lastcode = 0;
}
else
{
lastcode = 1;
}
return true;
}
else if(strcmp(gargv[0], "export") == 0)
{
if(gargc == 2)
{
AddEnv(gargv[1]);
lastcode = 0;
}
else
{
lastcode = 2;
}
return true;
}
else if(strcmp(gargv[0], "env") == 0)
{
for(int i = 0; genv[i]; i++)
{
printf("%s\n", genv[i]);
}
lastcode = 0;
return true;
}
else if(strcmp(gargv[0], "echo") == 0)
{
if(gargc == 2)
{
if(gargv[1][0] == '$')
{
if(gargv[1][1] == '?')
{
printf("%d\n", lastcode);
lastcode = 0;
}
}
else
{
printf("%s\n", gargv[1]);
lastcode = 0;
}
}
else
{
lastcode = 3;
}
return true;
}
return false;
}
初始化环境变量
void InitEnv()
{
extern char **environ;
int index = 0;
while(environ[index])
{
genv[index] = (char*)malloc(strlen(environ[index]) + 1);
strncpy(genv[index], environ[index], strlen(environ[index]) + 1);
index++;
}
genv[index] = nullptr;
}
main 主循环
int main()
{
InitEnv();
char command_buffer[basesize];
while(true)
{
PrintCommandLine();
if(!GetCommandLine(command_buffer, basesize))
{
continue;
}
ParseCommandLine(command_buffer, strlen(command_buffer));
if(CheckAndExecBuiltCommand())
{
continue;
}
ExecuteCommand();
}
return 0;
}
5.4 简易 shell 总结
- 普通命令:fork + exec + wait
- 内建命令:shell 自己直接处理
- 环境变量:从父 shell 继承并维护
$?:记录上一个命令的退出状态
shell 的本质,就是不断地解析命令并决定由谁执行。
六、总结
- fork 负责创建进程
- 写时拷贝保证父子进程初始共享、写入后分离
- exit 和 _exit 负责终止进程
- wait 和 waitpid 负责等待和回收子进程
- exec 族负责程序替换
- shell 通过 fork、exec、wait 组合完成命令执行
- 环境变量在 shell 和进程控制中起着重要作用

完
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)