【Linux】进程控制
本篇文章主要讲解进程控制相关系统调用,包括子进程创建、进程退出、进程等待与进程替换等
目录
1 创建子进程
创建子进程主要是通过 fork 函数来创建的,之前我们已经讲解过了,这里我们再回顾以及补充一下。
fork 系统调用
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
功能:为调用的当前进程创建一个子进程
返回值:
创建失败返回 -1; 创建成功子进程返回0, 父进程返回子进程的pid
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("before create child: %d\n", getpid());
//创建子进程
pid_t id = fork();
if (id == -1)
{
perror("fork");
return 1;
}
printf("after create child: %d\n", getpid());
return 0;
}

这里对于 fork 有那么几个问题:
问题1:为什么要给父进程返回子进程的 pid?
解答:因为父进程可以创建多个子进程,父进程:子进程=1:n,而 pid 是进程标识符,有了 pid 父进程就可以控制子进程了,比如回收进程各种内核数据结构,所以给父进程返回子进程 pid 是方便父进程来控制子进程。
问题2:一个函数是如何返回两个值的?
解答:函数的返回值一般都是放在CPU寄存器中,函数调用结束后,主函数会从寄存器中拿取函数的返回值。而调用 fork 执行 return 语句之前,子进程实际上已经创建成功了,拥有了独立的进程地址空间(进程地址空间,也就是虚拟内存到后面会进行讲解,这里可以理解为每个进程都拥有独立的虚拟地址空间,注意是虚拟并不是真实的物理内存空间),此时只需要修改子进程对应返回值的寄存器,将其值设为0。父进程返回值的寄存器值依然是子进程的 pid,这样就可以做到给不同进程返回不同的值了。
问题3:一个变量 id 是如何同时拥有两个值的?
解答:这个问题跟进程地址空间有关,这里我们先浅浅理解一下。子进程创建之后呢,子进程与父进程拥有独立的虚拟地址空间,注意是虚拟地址空间,不是物理地址空间。所以 id 地址相同实际上是虚拟地址空间,但是真正存放 id 变量的实际上是两个不同的物理地址空间,所以 id 看上去是一个变量,实际上在父子进程中是用不同的物理地址空间存放的。
fork 之后的流程
在 fork 之后会依次进行如下过程:
(1) 内核首先会分配新的内核数据结构(比如 task_struct)给子进程
(2) 将父进程的部分数据结构的内容拷贝给子进程
(3) 将子进程的状态设为就绪状态(R),为其分配一个 pid
(4) 复制父进程的进程上下文数据,以至于子进程可以从 fork 代码之后正常执行;为子进程设置 return 返回值
(5) fork 返回,开始调度器调度进程。注意,父子进程谁先运行完是由调度器决定的,并不是谁先创建谁就先运行完。
其余与 fork 相关的内容都与进程地址空间相关,在进程地址空间中我们再进行讲解。
2 进程退出
退出码
一个进程有没有正常退出我们是通过其退出码来确定的,这里的退出码也就是我们之前了解过的进程的错误码,我们可以使用 strerror 函数将错误码转换为字符串描述:
#include <stdio.h>
#include <string.h>
int main()
{
for (int i = 0; i < 200; i++)
printf("%d: %s\n", i, strerror(i));
return 0;
}

上面的各种错误信息前面的数字正是其对应的错误码。一般我们如果输入某个命令出错了会打印出一些错误信息,这些错误信息正是根据其错误码打印出来的。例如:

我们使用 ls 命令查看一个不存在的目录,可以发现 ls 报出的错误是 No such file or directory,正是 2 号错误码转成的字符串描述,那是不是呢?
我们可以在命令行中输入如下命令来查看上一个程序的退出码:
echo $?
之前我们说过,命令也是可执行程序,所以其也是有退出码的。所以我们可以通过 echo $? 来查看 ls 的退出码:

查看之后果然是2,那么如果我们查看一个存在的目录或者文件,ls 会执行成功,按照上面的退出码,ls 的退出码应该是 0:

当然上面的退出码是系统默认默认的退出码,我们也可以通过别的方法来自己设定程序的退出码,比如成功返回1,发生某种错误返回2,可以自己设置退出码,这样就可以根据退出码做出相应的判断了。
退出情况
对于一个程序来说,其退出情况一共有三种:
(1) 程序正常结束且结果正确,退出码一般为 0
(2) 程序正常结束但是结果不正确,退出码一般不为 0
(3) 程序由于发生某种错误,比如野指针访问、除0等,异常退出,退出信号不为 0
所以对于一个程序来说,程序正常还是异常退出,其实看的是退出信号,如果程序异常退出了,退出码是没有意义的;而一个程序正常退出之后,结果对还是不对,看的是退出码。
进程退出的三种方式
(1) main 函数 return 返回
第一种进程退出的方式就是我们直接在 main 函数里面 return 返回一个值。之前我们总喜欢在 main 函数最后写一句 return 0,其实就是在返回我们当前程序的退出码:
#include <stdio.h>
int main()
{
//...
return 0;
}

那如果我们改成 return 1,那么退出码其实就会变成1:
#include <stdio.h>
int main()
{
//...
return 1;
}

那么为什么我们习惯都会写成 return 0 呢?很简单,就是因为系统默认成功的退出码就是0,是一种默认写法,便于与系统或者其他程序协作。
(2) _exit 系统调用退出
在 Linux 中,也有一个系统调用是跟进程退出相关的,那就是 _exit 系统调用:
#include <unistd.h>
void _exit(int status);
参数:
status: 进程的退出码
示例:
#include <stdio.h>
#include <unistd.h>
int main()
{
sleep(3);
_exit(0);
}

但是当我们将 status 设为 -1 时,进程的退出码竟然变成了 255:
#include <stdio.h>
#include <unistd.h>
int main()
{
sleep(3);
_exit(-1);
}

这是因为虽然 status 是一个 int 值,但是系统在获取退出码时只会取其低 8 位,并且将其视为一个unsigned char,所以虽然传入的是 -1,但是在内存中存储的补码,所以 -1 其实是 11111111 11111111 11111111 11111111,取其低 8 位,就是 11111111,作为 unsigned char,其实就是 255 了。那么如果是 status = -2 呢?其实就是 254 了:
#include <stdio.h>
#include <unistd.h>
int main()
{
sleep(3);
_exit(-2);
}

(3) exit 库函数退出
在 C 语言中也有一个库函数 exit 是用来进行进程退出的:
#include <stdlib.h>
void exit(int status);
参数:
status: 进程的退出码
这里的 status 与 _exit 中的 status 相同,系统只会取其低 8 位,并且当做 unsigned char 输出。
其实在 main 函数中使用 return 退出,如果 return -1,退出码也是 255。所以不管是使用哪种方式进行进程退出,其退出码都是 0 ~ 255。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
sleep(3);
exit(-1);
}

exit 与 _exit 区别
进程退出内核会进行一系列的清理操作,比如清理进程的 task_struct。但是 exit 是一个库函数,而进程是系统资源,所以 exit 库函数想要让进程退出必须要通过内核,而访问内核又必须通过系统调用,所以 exit 的底层必然是封装了 _exit 系统调用的,这样才能完成进程退出功能。但是 exit 函数呢又不仅仅是调用了 _exit 系统调用,他还会完成一些别的工作。
(1) 调用用户定义的清理函数
这里我们使用 C++ 中类的析构函数来展示:
//test.cpp
#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>
class A
{
public:
A(const std::string& name):_name(name){}
~A()
{
std::cout << _name << "~A()" << std::endl;
}
private:
std::string _name;
};
//创建全局对象
A ga("全局对象");
int main()
{
//创建局部对象
A a("局部对象");
//创建静态对象
static A sa("静态对象");
exit(0);
return 0;
}
在这里呢我们构建了全局、局部与静态三个类对象,我们在 main 函数结束之前调用了 exit 函数,我们可以看到全局和静态对象被析构了,但是局部对象没有析构:

这是因为调用了 exit 之后,就会进入到 exit 函数的逻辑,执行完之后进程直接退出了,并不会返回 main 函数继续执行后续代码,那么 main 函数中的局部对象也就没有等到 main 函数的作用域结束,所以并不会发生析构;而全局对象和静态对象的生命周期是随进程的,在进程正常退出之前,回去调用他们的析构函数进行资源的清理工作。
那么如果我们将 exit 换为 _exit 系统调用呢?
#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>
class A
{
public:
A(const std::string& name):_name(name){}
~A()
{
std::cout << _name << "~A()" << std::endl;
}
private:
std::string _name;
};
//创建全局对象
A ga("全局对象");
int main()
{
//创建局部对象
A a("局部对象");
//创建静态对象
static A sa("静态对象");
_exit(0);
return 0;
}

我们可以看到并没有进行资源的清理工作,所以 _exit 并不会进行资源清理工作。
(2) 刷新用户(语言)级别的缓冲区
我们直接来看下面这两段代码的对比:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("hello");
exit(0);
return 0;
}

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("hello");
_exit(0);
return 0;
}

可以看到调用 exit 会输出 hello,但是 _exit 就没有。产生这种情况的原因就是因为语言级缓冲区的存在:
#include <stdio.h>
int main()
{
printf("hello world\n");
printf("hello world");
sleep(2);
return 0;
}
运行一下这个程序,发现第一个 "hello world" 一下就刷出来了,第二个 "hello world" 要等上 2 秒钟才会刷出来。其实我们在使用 printf 库函数的时候,其实这个函数里面是会有一个缓冲区的(一块内存空间),这个缓冲区就称为用户级别缓冲区,或者语言级别缓冲区。而由于 printf 函数是打印到屏幕上,对于屏幕采用的是行刷新策略。所以一旦我们在后面加上 '\n',会将用户级缓冲区中的内容立即刷新到内核,也就看到了第一个 "hello world",但是第二个 "hello world" 没有加 '\n',所以需要等待进程结束才会进行刷新,所以要等 2 s 才能刷新出来。
而 exit 与 _exit 的区别就是 exit 退出时会刷新这个用户级别缓冲区,而 _exit 不会。所以为了进程退出时能够清理资源并且刷新缓冲区,我们推荐进程退出时使用 exit 函数。
3 进程等待
进程等待的必要性
进程等待是指父进程通过系统调用进入阻塞状态,并且主动等待一个或者多个子进程运行结束,之后回收子进程剩余资源的过程。进程等待是一定要做的,那么为什么父进程要等待子进程呢?主要有两点原因,也是进程等待主要完成的两件事情:
(1) 回收子进程的僵尸状态
之前我们在进程状态中讲解过,进程运行结束后会先进入一种僵尸状态,被称之为僵尸进程,僵尸进程在某种意义上已经死了,我们是无法通过 kill -9 杀死他的,而且僵尸进程会占用系统内存资源,造成内存泄露问题。所以如果系统中有大量的僵尸进程,系统可能会因为内存资源不足影响运行。而父进程一旦通过进程等待检查到了子进程的僵尸状态,就会回收子进程的各种信息,然后回收子进程,避免子进程一直为僵尸进程。
(2) 获取子进程的退出状态
我们在退出码小节讲解过,进程是正常还是异常退出是通过退出信号来看的,退出信号为 0 代表正常退出,退出信号不为 0 代表异常退出。而正常退出后,运行结果是否正确是根据退出码来看的,一般退出码为 0,代表结果正确,不为 0,代表结果错误。
那么子进程将任务完成的怎么样,我们是需要知道的,这个退出状态是保存在子进程的 task_struct 中的,所以父进程就要通过进程等待来获取子进程的退出状态,以便于我们做出判断。
所以父进程,为了进行进程等待,一般都是最先创建最后退出的。
进程等待的方法
我们主要通过两个系统调用来进行进程等待:wait 与 waitpid 系统调用。
wait 系统调用
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
参数:
wstatus: 输出型参数,主要是获取子进程的退出状态,不关心可以设为 NULL
返回值:成功返回子进程的 pid,失败返回 -1
等待单个子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//创建子进程
pid_t id = fork();
if (id == 0)
{
//child
printf("I am child, pid: %d\n", getpid());
sleep(2);
exit(0);
}
//使用 wait 来等待子进程
pid_t rid = wait(NULL);
if (rid == -1)
{
perror("wait");
return 1;
}
printf("子进程等待成功, pid: %d\n", rid);
return 0;
}

等待多个子进程
上面是等待一个子进程的情况,那么我们要是等待多个子进程呢?如果我们只 wait 一次就会出现如下情况:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
const int childnum = 10;
int main()
{
// 创建多个子进程
for (int i = 0; i < childnum; i++)
{
pid_t id = fork();
if (id == 0)
{
// child
printf("I am child, pid: %d\n", getpid());
sleep(2);
exit(0);
}
}
pid_t rid = wait(NULL);
if (rid == -1)
{
perror("wait");
return 1;
}
printf("子进程等待成功, pid: %d\n", rid);
return 0;
}

我们可以看到,父进程只等待了一个子进程就直接退出了,所以 wait 函数每次只会等待一个子进程,当等待到一个子进程之后直接就会退出了,所以我们需要循环等待多个子进程:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
const int childnum = 10;
int main()
{
// 创建多个子进程
for (int i = 0; i < childnum; i++)
{
pid_t id = fork();
if (id == 0)
{
// child
printf("I am child, pid: %d\n", getpid());
sleep(2);
exit(0);
}
}
//使用 wait 来等待子进程
for (int i = 0; i < childnum; i++)
{
pid_t rid = wait(NULL);
if (rid == -1)
{
perror("wait");
return 1;
}
printf("子进程等待成功, pid: %d\n", rid);
}
return 0;
}

waitpid 系统调用
上面的 wait 是随机等待任意一个子进程的,那么如果我们想要等待一个特定的子进程呢?这时候我们就需要使用 waitpid 接口了:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
参数:
pid: 等待进程的 pid,设为 -1 表示等待任意一个子进程,等价于 wait
wstatus: 与 wait 参数相同,等待进程的退出状态
options: 设为 0 表示阻塞等待,设为 WNOHANG 表示非阻塞等待
返回值:成功时返回等待进程的 pid,如果失败返回 -1; 如果设置了 WNOHANG, 那么父进程发现没有子进程在等待,那就会返回 0
等待单个子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
const int childnum = 10;
int main()
{
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// child
printf("I am child, pid: %d\n", getpid());
sleep(2);
exit(0);
}
// 使用 waitpid 来等待子进程,第一个可以设为-1或者id,0 表示阻塞等待
pid_t rid = waitpid(id, NULL, 0);
if (rid == -1)
{
perror("waitpid");
return 1;
}
printf("子进程等待成功, pid: %d\n", rid);
return 0;
}

等待多个子进程
在等待多个子进程时,我们可以将 waitpid 中的第一个参数改为 -1,表示等待任意一个子进程,而且 waitpid 与 wait 相同,只会等待一个子进程,所以我们依然需要循环来等待所有子进程:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
const int childnum = 10;
int main()
{
// 创建多个子进程
for (int i = 0; i < childnum; i++)
{
pid_t id = fork();
if (id == 0)
{
// child
printf("I am child, pid: %d\n", getpid());
sleep(2);
exit(0);
}
}
// 使用 waitpid 来等待子进程
for (int i = 0;i < childnum; i++)
{
pid_t rid = waitpid(-1, NULL, 0);
if (rid == -1)
{
perror("waitpid");
return 1;
}
printf("子进程等待成功, pid: %d\n", rid);
}
return 0;
}

waitpid 的功能比 wait 更全,wait 相当于 waitpid 的子功能。waitpid 还可以等待指定的一个子进程,并且可以选择阻塞或者非阻塞等待,所以以后在进程等待时我们就使用 waitpid,不使用 wait 了。
获取子进程的退出状态
在 waitpid 与 wait 系统调用中,有一个参数叫做 wstatus,这个参数就是用来获取子进程退出状态的。
wstatus 是一个输出型参数,类型是 int*。当我们定义一个整形变量 status 传递进去地址之后,等待成功之后会将进程的退出状态写入到我们传入 status 变量中,我们就可以获取子进程的退出状态了。*wstatus 变量循环进程的退出信息是这样的:

*wstatus 为 int 类型变量,共有 32 位。但是会用到的只有低 16 位,高 16 位是用不到的。在这低 16 位中,低 7 位表示退出信号,第 8 位表示 coredump 标志位,次低 8 位表示退出码。所以进程有没有正常退出,我们看的是 *wstatus 中的低 7 位;而正常退出后运行结果正不正确我们用的是次低 8 位。而那个 coredump 标志位我们的等到进程信号时会进行讲解。
所以要想获得进程的退出信息,我们需要进行的是位操作:
退出码: (status >> 8) & 0xFF
退出信号: status & 0x7F
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
const int childnum = 10;
int main()
{
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// child
printf("I am child, pid: %d\n", getpid());
sleep(2);
exit(0);
}
//使用 waitpid 来等待子进程
int status = 0;
pid_t rid = waitpid(-1, &status, 0);
if (rid == -1)
{
perror("waitpid");
return 1;
}
//打印子进程退出状态
printf("子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n", rid, ((status >> 8) & 0xFF), (status & 0x7F));
return 0;
}

我们可以将退出码设为 1:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
const int childnum = 10;
int main()
{
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// child
printf("I am child, pid: %d\n", getpid());
sleep(2);
exit(1);
}
//使用 waitpid 来等待子进程
int status = 0;
pid_t rid = waitpid(-1, &status, 0);
if (rid == -1)
{
perror("waitpid");
return 1;
}
//打印子进程退出状态
printf("子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n", rid, ((status >> 8) & 0xFF), (status & 0x7F));
return 0;
}

让子进程异常退出:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
const int childnum = 10;
int main()
{
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// child
printf("I am child, pid: %d\n", getpid());
sleep(2);
int* ptr = NULL;
*ptr = 10;
exit(1);
}
//使用 waitpid 来等待子进程
int status = 0;
pid_t rid = waitpid(-1, &status, 0);
if (rid == -1)
{
perror("waitpid");
return 1;
}
//打印子进程退出状态
printf("子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n", rid, ((status >> 8) & 0xFF), (status & 0x7F));
return 0;
}

所以我们就可以通过 status 来获取进程的退出状态了,我们就可以通过 status 变量来知道子进程的运行结果,从而做出相应判断了。
但是我们提取子进程状态时每次都需要进行位操作,很麻烦,所以系统中位我们提供了两个宏方便我们进行操作:
WIFEXITED(status): 用来查看进程是否正常退出,如果正常退出返回真,异常退出返回假
WEXITSTATUS(status): 提取子进程的退出码
我们可以查看一下这两个宏在系统中的定义:
需要注意的就是 WIFEXITED(status) 返回的是进程是否正确退出,返回的是一个逻辑值并不是真正的退出信号。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// child
printf("I am child, pid: %d\n", getpid());
sleep(2);
int *ptr = NULL;
*ptr = 1;
exit(1);
}
// 使用 waitpid 来等待子进程
int status = 0;
pid_t rid = waitpid(-1, &status, 0);
if (rid == -1)
{
perror("waitpid");
return 1;
}
if (WIFEXITED(status))
{
printf("进程正常退出\n");
printf("子进程等待成功, pid: %d, exit code: %d\n", rid, WEXITSTATUS(status));
}
else
{
printf("进程异常退出, exit signal: %d\n", (status & 0x7F));
}
return 0;
}

阻塞等待与非阻塞等待
在 waitpid 中还有一个参数,就是第三个参数 options,选择是阻塞等待还是非阻塞等待。
4 进程替换
我们在创建子进程小节中讲过,fork 之后,父子进程执行的是父进程 fork 代码之后相同的代码,那么子进程要是想要执行别的代码,该怎么办呢?这时候我们就需要使用进程替换了。
进程替换原理
之前我们在 C 语言阶段了解过内存的分布,包括代码段、数据段、堆取、栈区,其实在堆栈之间还有一个共享区:

所谓的进程替换只不过就是把子进程中的代码段和数据段替换成磁盘中的别的代码和数据:

所以进程替换是不需要修改内核中进程管理的其他数据数据结构的,只需要将磁盘中新的代码和数据替换掉内存中跟需要进行进程替换的进程的代码和数据,即可完成程序替换。
上面的图片是为了讲解进程替换原理画出的一张图,在我们学习完进程地址空间,也就是虚拟内存之后,程序替换的真实原理其实是:

不过这个需要我们到了虚拟内存讲解时才能了解了。
进程替换接口使用
对于进程替换来说,其接口都是以 exec 开头的,所以称为 exec 系列函数,其中有六个库函数:
#include <unistd.h>
int execl(const char *pathname, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *pathname, const char *arg, ...);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
还有一个系统调用:
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
别看 exec 系列函数很多,但其实学会了一个,再结合命名,很快就可以学会全部的函数了。
(1) execl 函数
execl 函数中的 l 是 list 的意思,意思是我们需要将后面的参数像列表一样一个一个列出来。下面是这个函数的具体使用方法:
#include <unistd.h>
int execl(const char *pathname, const char *arg, ...);
参数:
pathname: 需要替换的二进制文件名称,注意需要带路径
arg: 是如何执行二进制程序,注意最后必须以 NULL 结尾。
这里替换的时候我们命令行怎么执行,这里就怎么传递参数就可以。
返回值:成功时没有返回值,失败时返回 -1
exec 系列函数如果替换成功了,是不会有返回值的。因为一旦替换成功,就会去执行替换后的代码,当前代码就不会执行了,那么返回值其实也就没什么用了。
替换为自己的程序:
//exec.c
#include <stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
//test.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// child
printf("I am child, pid: %d\n", getpid());
//使用 execl 将子进程替换为 ls -a -l 程序
//注意以 NULL 结尾
//程序必须带路径
int n = execl("./exec", "./exec", NULL);
//这里可以判断,也可以不判断,因为程序替换成功,就不会执行后续代码了
//if (n < 0)
perror("execl");
exit(1);
}
printf("我是父进程, pid: %d\n", getpid());
//父进程执行完,需要等待子进程
int status = 0;
int rid = waitpid(-1, &status, 0);
if (rid < 0)
{
perror("waitpid");
return 1;
}
printf("子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n", rid, WEXITSTATUS(status), (status & 0x7F));
return 0;
}
注意在这里为了清晰看到程序替换的效果,故意将 exec.c 中的退出码写为了 1:

替换为系统程序(各种命令):
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// child
printf("I am child, pid: %d\n", getpid());
//使用 execl 将子进程替换为 ls -a -l 程序
//注意以 NULL 结尾
//ls 必须带路径
int n = execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
//这里可以判断,也可以不判断,因为程序替换成功,就不会执行后续代码了
//if (n < 0)
perror("execl");
exit(1);
}
printf("我是父进程, pid: %d\n", getpid());
//父进程执行完,需要等待子进程
int status = 0;
int rid = waitpid(-1, &status, 0);
if (rid < 0)
{
perror("waitpid");
return 1;
}
printf("子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n", rid, WEXITSTATUS(status), (status & 0x7F));
return 0;
}

(2) 其他 exec 系列函数的使用
有了 execl 的使用经历之后,结合命名就可以很快掌握其他函数了:
| 符号 | 全称 | 规律 |
|---|---|---|
| l | list(列表) | 传入的参数用列表的形式 |
| v | vector(数组) | 传入的参数使用指针数组,注意以NULL结尾。一般与命令行参数 argv[] 数组配合使用 |
| p | PATH(路径) | 会自动搜索环境变量PATH,第一个参数就不用带路径了 |
| e | environment(环境变量) | 一般是自己传入环境变量 |
结合上面的命名规律就很好,再结合之前的 execl 使用,很容易就上手了。比如:
execlp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// child
printf("I am child, pid: %d\n", getpid());
//使用 execlp 将子进程替换为 ls -a -l 程序
//注意以 NULL 结尾
//这里的 ls 就不必带路径了,因为 PATH 中带有 /usr/bin,所以会自动搜索
int n = execlp("ls", "ls", "-a", "-l", NULL);
//这里可以判断,也可以不判断,因为程序替换成功,就不会执行后续代码了
//if (n < 0)
perror("execlp");
exit(1);
}
printf("我是父进程, pid: %d\n", getpid());
//父进程执行完,需要等待子进程
int status = 0;
int rid = waitpid(-1, &status, 0);
if (rid < 0)
{
perror("waitpid");
return 1;
}
printf("子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n", rid, WEXITSTATUS(status), (status & 0x7F));
return 0;
}

但是需要注意的是,虽然其会自动搜索 PATH 环境变量,但是我们替换为我们自己的程序时,依然需要带上路径,因为 PATH 环境变量中并没有我们自己的路径。如果想要不带路径,那就必须将我们自己程序的路径添加到 PATH 环境变量中。
execvp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// child
printf("I am child, pid: %d\n", getpid());
//使用 execvp 将子进程替换为 ls -a -l 程序
//注意以 NULL 结尾
//这里的 ls 就不必带路径了,因为 PATH 中带有 /usr/bin,所以会自动搜索
//execvp 注意第二个参数传入的是一个数组
char* argv[] = {"ls", "-a", "-l", NULL};
int n = execvp("ls", argv);
//这里可以判断,也可以不判断,因为程序替换成功,就不会执行后续代码了
//if (n < 0)
perror("execlp");
exit(1);
}
printf("我是父进程, pid: %d\n", getpid());
//父进程执行完,需要等待子进程
int status = 0;
int rid = waitpid(-1, &status, 0);
if (rid < 0)
{
perror("waitpid");
return 1;
}
printf("子进程等待成功, pid: %d, exit code: %d, exit signal: %d\n", rid, WEXITSTATUS(status), (status & 0x7F));
return 0;
}

通过上面两个示例,相信大家已经懂得 p、v 这两个如何使用了,e 在日常中使用的并不多,这里就不进行讲解了,接下来大家可以自行组合了。
注意在 exec 系列函数中,只有一个系统调用 execve,其余的都是库函数。所以除了 execve,其余的函数底层都是调用了 execve 系统调用的,
5 总结
本篇文章中我们讲解了进程控制的四大模块,包括创建子进程、进程退出、进程等待与进程替换,对应的最佳接口分别是 fork、exit、waitpid 以及 exec 系列函数。创建子进程后,父子进程共享 fork 之后的代码,如果想要替换子进程中的代码,就需要使用 exec 系列函数来进行进程替换。而子进程一旦退出,如果父进程不回收他,那么子进程就会变成僵尸进程造成内存泄露现象,所以父进程需要等待子进程,防止这种现象的发生并且回收子进程的退出状态。
下一篇文章中我们会利用进程控制与环境变量、命令行参数来写一个小的 shell 程序,通过这个程序我们不仅可以熟悉接口,还可以了解命令行解释器 bash 是如何运行的。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)