前面一章「【Linux】进程详解一:进程概念」 中我们已经了解过了进程的基本概念,这一章我们要进一步的学习进程,即**「进程的控制」**。
在已经认识了进程的基本概念之后,我们首先需要来自己创建一个进程。
创建进程有两种创建方式:
1.使用./
运行某一个可执行程序,这种是最常见的方式
2.使用系统调用接口创建进程,即使用fork()
,fork()
函数可以帮助我们从原来的进程中创建一个新的子进程,而原来的进程就被叫做父进程。
fork()
函数的语法规则:
#include
pid_t fork(); // 返回值有两个:子进程返回0,父进程返回子进程的PID,如果子进程创建失败返回-1
注意:返回值有两个:子进程返回0,父进程返回子进程的PID,如果子进程创建失败返回-1(后面会解释为什么fork()
函数会有两个返回值,现在只要了解fork()
函数之后有父子进程都会有返回值即可)
废话不多说,我们先来看看如何使用fork()
,之后我们再来观察fork()
中干了什么,以及父子进程的关系。
#include
#include
#include
int main()
{
printf("Before:PID is %d\n", getpid()); // 在fork之前打印一句话
pid_t pid = fork();
if (pid == -1) {
printf("fork error!\n");
exit(1);
}
printf("After:PID is %d, return is %d\n", getpid(), pid); // 在fork之后打印一句话
sleep(1);
return 0;
}
运行结果:
案例解释:运行的结构为fork()
之前只有一个进程,所以只打印出了一句话。而fork()
之后,多出了一个新的进程,所以会打印出两句话,并且两个进程返回值不一样,其中返回值为0的进程为子进程,返回值非0的进程为父进程。
注意:在fork()
之后,父子进程是相互独立的两个进程,所以两个进程的执行顺序是不能确定的。完全取决于调度器的调度。
在一个进程中调用fork()的时候,内核中做了哪些工作?
1.给子进程分配内存块和task_struct
和mm_struct
等数据结构。
2.将父进程中部分数据结构的内容拷贝一份到子进程中。
3.添加子进程中系统进程列表中
4.fork()
返回相应的返回值,调度器开始调度。
其实知道了fork
函数内部的执行的过程,就知道了在fork
内部的时候,就已经创建了一个新的进程,所以新的进程会有一个返回值,而父进程也会有一个返回值,所以fork
创建一个进程会有两个返回值。
新创建的子进程机会和父进程一模一样,但是还是不完全一样。
上面说过了,fork()
函数会有两个返回值,子进程返回0,父进程返回子进程的PID,下面就需要解决两个问题。第一:fork()
为什么会有两个返回值。第二:为什么要返回值是返回0和PID。
fork()
函数为什么会有两个返回值?根据fork()
函数在内核中的操作就包含了子进程的数据结构的创建,所以**在fork()
返回之前,子进程就已经被创建出来了。而一旦被创建出来一个独立的进程就会有返回值,所以调用这个fork()
函数的父进程有一个返回值,而创建出的子进程也会有一个返回值。**因为这两个过程是在fork()
函数内部就已经完成了,因此我们在fork()
函数外面看到的现象就是一个函数出现了两个返回值。
fork()
函数中,子进程要返回0,而父进程要返回子进程的PID?一个父进程可以创建很多的子进程,而每一个子进程都只能有一个父进程。**而父进程创建子进程是为了让子进程完成任务的,所以父进程需要标志每一个子进程,所以父进程通过返回子进程的PID来标识每一个子进程。**而子进程只有唯一的父进程,所以不需要标识父进程,因此返回一个0就可以了。
一开始创建子进程的时候,子进程和父进程的代码和数据共享,即相同的虚拟地址会映射到相同的物理地址空间。 当在子进程要修改父进程中的数据的时候,父进程中的数据会重新的拷贝一份,然后子进程再对数据进行修改。这样父子进程中的数据就独立了。
对于写时拷贝,有三个问题要注意:
1.为什么要进行写时拷贝?
进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。
2.为什么不在创建子进程的时候就直接在子进程中拷贝一份父进程中的data和code?
子进程不一定会修改父进程中的code或者data,只有当需要修改的时候,拷贝父进程中的数据才会有意义,这种按需分配的方式,也是一种延时分配,可以高效的时候使用内存空间和运行的效率。
3.父进程的代码段会不会进行拷贝?
一般情况下,子进程只会修改父进程副本的数据,不会对父进程的代码进行什么操作。但是当在进程替换的时候,子进程会拷贝一份父进程的代码段。
1.一个进程希望有多个进程执行一段代码的不同部分。
2.可以在一个进程中调用另一个进程,可以通过进程替换exec
系列函数实现。
一般情况下fork()
函数不会调用失败,但是有两个情况下会使得fork()
创建子进程失败:
1.系统中已经存在了很多的进程,内存空间不足以再创建进程了
2.实际用户的进程超过了限制
了解进程创建之后,我们就要来了解一个进程的终止。
本节会介绍进程终止退出的场景,然后再分别介绍进程两种进程退出的方法。
进程需要终止退出的情况有三种:
1.代码运行完毕,并且运行结果正确。(进程正常终止)
2.代码运行完毕,并且运行结果不正确。(进程正常终止)
3.进程崩溃(进程异常终止)。
得到进程退出码有不止一种方式,但是这里介绍一种大家最熟悉的得到进程退出码的方式。
我们平时如果想要写一个C/C++
程序的代码,写的第一个函数一定是main()
,而main()
是由返回值的。而**所谓的进程退出码就是以main()
函数的返回值的形式返回的。退出码为0表示代码执行成功,退出码为非0表示代码执行失败。**所以一般情况下,main()
函数返回0,以表示代码执行成功。
下面两个问题可以帮助你更好地理解进程退出码的意义。
1.
main()
的返回值给了谁?
main()
函数也是一个函数,既然函数有返回值,那么该函数返回给了谁呢?要想搞清楚这个问题,就需要搞清楚到底是谁调用了main()
函数。不同的平台下调用main()
函数的函数不同,但是最终main()
函数是由系统间接调用的,所以其实main()
的返回值返回给了操作系统。
2.为什么
main()
函数要有返回值或者进程要有退出码?
一个程序被加载到内存上,形成进程,是用来完成某项任务的。当进程完成任务后,我们需要知道进程完成任务的情况,因此需要通过退出码这种形式来得知进程执行任务的情况。
3.为什么退出码为0表示执行成功,非0表示执行错误?
前面说了进程需要通过进程退出码的性质告诉外界自己完成任务的情况。
如果进程成功的执行完任务正常退出,这种情况很好。而且这种情况值唯一的,所以用0就可以表示了。
但是如果进程非正常退出,那么我们就需要知道进程为什么不正常退出,这时情况就比较复杂了,不正常退出的情况有很多, 例如内存空间不足、非法访问以及栈溢出等等。所以非正常退出的原因需要很多的数字来表示,因此就使用了非0来表示。
进程退出码有很多,每一个退出码都有对应的字符串含义,帮助用户确认执行失败的原因。
我们可以使用$?
来查看最近一个进程的退出码。
echo $? # 打印出最近一个进程的退出码
例如:
如果想要知道每一种的进程退出码的含义, C语言当中的strerror函数可以通过错误码,获取该错误码在C语言当中对应的错误信息:
#include
#include
int main()
{
for (int i = 0; i < 100; i ++) {
printf("第%d中进程退出码的含义: %s\n", i, strerror(i));
}
return 0;
}
运行结果:
注意:这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。
正常终止进程一般由3种方法。
最常用的退出方式就是在main()
最后通过return
的方式退出进程。
例如:
#include
int main()
{
printf("hello world\n");
return 0;
}
查看进程退出码:
exit()
函数和return
的功能差不多,但是exit()
在任何的地方只要被调用,就会立即的退出进程。
注意和return
的区别:只有在main()
函数中return
才会退出进程,而exit()
在任意一个函数中都可以退出进程。
例如:
#include
#include
void func()
{
printf("I am exiting!"); // 这里没有使用\n来刷新缓冲区
exit(1);
}
int main()
{
func();
}
运行结果并且查看进程退出码:
由上面这一段可以看出:
1.exit()
可以像return
返回进程退出码。
2.原本在缓冲区中的内容,在exit()
之后被刷新出来了,所以exit()
其实在退出之前,刷新了缓冲区。
_exit()
其实是封装在exit()
函数中的,和exit()
的区别在于_exit()
在退出之前不会刷新缓冲区,而是直接退出。
#include
#include
#include
void func()
{
printf("I am exiting!"); // 这里没有使用\n来刷新缓冲区
_exit(1); // 和exit()那一段代码相比,只有这一部分不同
}
int main()
{
func();
}
运行结果并且查看退出码:
通过运行结果可知:
_exit()
函数不会在退出进程前刷新缓冲区。
return
,exit()
和_exit()
相似点:
通过return
,exit()
和_exit()
都可以得到退出码。
不同点:
return
:return
只能在main()
函数中返回才可以退出进程。
exit()
:exit()
可以在任何的地方随时的退出进程,并且在退出进程前会刷新缓冲区。
_exit()
:_exit()
可以在任何的地方随时的退出进程,但是在退出系统的时候,不会刷新缓冲区。
联系:
1.在main()
函数中的return
等价于exit()
2.在exit()
中封装了_exit()
函数。
进程也会异常退出:
情况1:向进程发起信号或者ctrl + c
直接终止进程。
使用kill -9 PID
或者ctrl + c
可以直接使得进程异常终止。
情况2:代码出现段错误导致运行异常退出。
例如:代码中有num/0
的情况或者出现了野指针的问题。
注意:当进程异常终止的时候,退出码就没有意义了。因为退出码是进程正常退出的返回的信息,对于异常终止的进程,退出码不能反映出进程的执行的情况。
「进程等待」的工作就是让父进程回收子进程的资源,获取子进程的退出信息。
因为如果子进程退出,父进程不读取子进程的退出信息回收子进程的资源的话,子进程就会变成僵尸进程,进而造成内存泄漏。而一个进程变成僵尸进程的时候,就算是使用kill -9
发送信号的方式也是不能回收该进程的资源的。
所以一定需要通过父进程通过进程等待的方式,来回收子进程的资源,同时为了搞清楚子进程完成任务的情况,也需要通过通过进程等待的方式获取子进程的退出信息。
我们需要通过pid_t wait(int* status);
函数和pid_t waitpid(pid_t pid, int* status, int options);
函数来做进程等待。
而在学习这两个函数之前,我们发现这两个函数中都有一个参数status
,而这个参数比较复杂且重要,所以我们先来学习一下status
这个参数。
status
的作用上文说过,进程等待不仅是回收子进程的资源也需要获取子进程的退出信息,所以 status
的作用就是获取退出的信息 。status
是一个输出型参数,即在wait()
函数外面的变量,传入wait()
函数,然后在wait()
内部对status
进行操作,从而改变status
。
注意:如果不想要获取进程的退出信息的话,就可以用NULL
替代status
。
status
的组成在status
的后16个比特位上,高8位表示进程退出的状态,即进程退出码。而后7位为进程终止的信号。第8个比特位是一个标志。
注意:当进程正常退出的时候,不用查看退出信号。而如果一个进程异常退出,即被信号杀死的话,不用看退出码。
status
中获取退出码和退出信号有两种方法我们可以获取status
中的退出信息。
方法一:位运算
既然我们已经知道了status
中的比特位组成部分,我们就可以通过位运算的操作直接获取退出信息。
int exit_code = (status >> 8) & 0xff; // 获取退出码
int exit_signal = status >> 0x7f; // 获取退出信号
方法二:使用宏
在系统中,提供了两个宏来获取提出码和退出信号。
WIFEXITED(status); // 用于查看进程是否正常退出,其实就是查看是否有退出信号
WEXITSTATUS(status); // 用于获取进程的退出码
再次提醒:如果一个进程被信号杀死,则退出码没有意义。
举例子:
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid == 0) {
// child
int count= 5;
while (count --) {
printf("I am child process, PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(10);
} else {
// father
int status = 0;
pid_t res = wait(&status);
if (res > 0) {
printf("wait child process success!\n");
printf("exit code1:%d, exit signal1:%d, exit code2:%d,exit signal2:%d\n", (status >> 8) & 0xff, (status) & 0x7f, WEXITSTATUS(status), WIFEXITED(status));
}
}
return 0;
}
运行结果:
下面来学习进程等待的两种方法:分别是调用wait()
函数和waitpid()
函数。
wait()
方法#include
#include
pid_t wait(int* status);
wait()
函数的作用是:等待任意的一个子进程
例如:
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid == 0) {
// child
int count= 5;
while (count --) {
printf("I am child process, PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(10);
} else {
// father
int status = 0;
pid_t res = wait(&status); // 如果wait成功,返回值为子进程的PID
sleep(10); // 这里休眠了10秒,可以看到子进程已经退出了并且没有被wait,这是进程变成僵尸进程
if (res > 0) {
printf("wait child process success!\n");
if (WIFEXITED(status)) {
// 正常退出
printf("exit code: %d\n", WEXITSTATUS(status));
} else {
// 异常退出
printf("exit signal:%d\n", status & 0x7f);
}
}
}
return 0;
}
运行结果:
这里是监控级进程的一个脚本:
while :; do ps -axj | head -1 && ps -axj | grep test | grep -v grep; sleep 1; done;
我们可以发现,在子进程运行完毕之后,因为父进程还在sleep()
,所以此时子进程变成了僵尸进程,而在父进程sleep()
完了之后,子进程的资源被wait()
完回收掉了,所以僵尸进程消失了。
waitpid()
方法#include
#include
pid_t waitpid(pid_t pid, int* status, int options);
waitpid()
函数的作用是:等待指定的一个子进程或者任意一个进程。(这个可以有options
参数控制)
函数的参数:
pid
:指定等待的子进程的PID
,如果PID
设置为-1
的话,则等待任意一个子进程。status
:输出型参数,获取子进程的退出信息,如果不需要进程退出的退出信息,可设置为NULL
。options
:当options
设置为0的时候,叫做阻塞等待。当options
设置为WNOWAIT
的时候,叫做非阻塞等待。(后面会有阻塞等待和非阻塞等待的例子)函数的返回值:
PID
。options
被设置为WNOWAIT
,但是并没有出现等待的子进程时,返回0。wait
失败,返回-1
。下面分别对阻塞等待和非阻塞等待举出一个例子:
在子进程运行的时候,父进程在干什么呢?如果父进程就在那里等待子进程完成任务,接收子进程的退出信息的话,这种方式就是阻塞等待。就好像父进程被阻塞住不能前进一样。。如果父进程在子进程运行的时候,自己可以感自己的事情,这种方式就叫做非阻塞等待。
所以想要判断是否为阻塞或者非阻塞等待,就只要判断父进程在子进程运行的时候,可不可以自己运行自己的代码即可。
下面两个案例,为了使我们看清楚,所以在子进程运行的时候sleep(1)
,这样就可以看清楚是否父进程在运行代码。
waitpid()
阻塞等待案例#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid == 0) {
// child
int count= 5;
while (count --) {
printf("I am child process, PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(10);
} else {
// father
int status = 0;
pid_t res = waitpid(pid, &status, 0); // 阻塞等待指定的子进程
if (res > 0) {
printf("wait child process success!\n");
if (WIFEXITED(status)) {
printf("exit code: %d\n", WEXITSTATUS(status));
} else {
printf("exit signal: %d\n", status & 0x7f);
}
}
}
return 0;
}
运行结果:
情况一:
情况二:
waitpid()
非阻塞等待案例当options
设置为WNOHANG
的时候,进程等待为非阻塞等待,此时父进程不会一个阻塞在一个地方等待子进程的退出,而是每隔一段时间检查是否子进程已经退出,如果子进程没有退出的话,waitpid
返回0
,此时父进程可以独立的执行自己的代码。
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid == 0) {
// child
int count= 5;
while (count --) {
printf("I am child process, PID:%d, PPID:%d\n", getpid(), getppid());
sleep(2);
}
exit(10);
} else {
while (1) {
int status = 0;
pid_t res = waitpid(pid, &status, WNOHANG);
if (res > 0) {
printf("wait child success!\n");
printf("exit code: %d\n", WEXITSTATUS(status));
break;
} else if (res == 0) {
printf("father process can do something!\n");
sleep(1);
}
}
}
return 0;
}
运行结果:
前面的案例都是单个进程的,实际上一个父进程可以创建多个进程并行的执行。
其实我们使用一个数组将创建的子进程的PID
保存起来,这样父进程就可以通过数组来获取每一个子进程的PID
了。
#include
#include
#include
#include
#include
int main()
{
pid_t pids[10];
for (int i = 0; i < 10; i ++) {
pid_t pid = fork();
if (pid == 0) {
// child
printf("child process create success!!!, PID:%d\n", getpid());
exit(i);
} else if (pid > 0) {
// father
pids[i] = pid; // 保存子进程的PID
}
}
for (int i = 0; i < 10; i ++) {
int status = 0;
pid_t res = waitpid(pids[i], &status, 0);
if (res > 0) {
printf("第%d个子进程已经wait成功, PID:%d\n", i, pids[i]);
if (WIFEXITED(status)) {
printf("exit code: %d\n", WEXITSTATUS(status));
} else {
printf("exit signal: %d\n", status & 0x7f);
}
}
}
}
运行结果:
最后我们来谈一谈最后一个进程控制就是「进程替换」。所谓进程替换,就是为了解决在一个进程中可以执行另一个进程的问题,而进程替换需要调用exec
系列的函数来完成这一过程。
exec
系列的函数怎么可以做到在一个进程中调用另一个进程呢?
其实原理也十分的简单,就是通过父进程创建出一个新的子进程,然后通过exec
函数,将子进程中的代码和数据都被你想要执行进程的代码和数据所替换掉即可,也就是将一个进程的代码和数据覆盖在另一个进程的代码和数据上。
要注意两个问题:
1、当进程被另一个进程替换时,并没有创建一个新的进程 而只是在原来的进程的基础上,在进程的物理内存中代码和数据被另一个进程的代码和数据段所替换而已。其余的数据结构类似PCB
,mm_struct
,页表等等结构并没有改变。
2.在子进程进行程序替换之后,父进程中的代码段和数据段并没有受到任何的影响。 这是因为当子进程在进行进程替换时,需要对进程的数据和代码段进程修改,这时进程会发生写时拷贝,而在写时拷贝之后,父子进程的代码和数据独立了,所以相互之间的数据和代码不会受到影响。
进程替换函数是exec
系列函数,而这一系列的函数一共有6个函数。
#include
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()
int execl(const char* path, const char* arg, ...);
path
:要替换的可执行程序所在的路径arg
:给可执行程序传递的命令行参数(是一个可变参数列表),最后要以NULL
结尾。execl
执行成功,则进程替换成功,那么就没有返回值,因为进程已经执行其他进程了。如果execl
执行失败,则返回-1
。举例:
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid == 0) {
// “/usr/bin/ls"是可执行文件的路径
// "ls", "-a", "-l", "-i"是执行的方式
// 就好像是直接在命令行中敲 ls -a -l -i样
execl("/usr/bin/ls", "ls", "-a", "-l", "-i", NULL);
exit(1);
} else {
// 进程等待,父进程回收子进程资源
int status = 0;
pid_t res = waitpid(pid, &status, 0);
if (res > 0) {
printf("wait child process success!\n");
if (WIFEXITED(status)) {
printf("exit code: %d\n", WEXITSTATUS(status));
} else {
printf("exit signal: %d\n", status & 0x7f);
}
}
}
return 0;
}
运行结果:
举例2:
如果你不想要只调用系统中的可执行程序,而你想要利用一个自己写的程序直接调用另一个自己写的程序的话也是可以的。
我们先写一个程序,假设是一个C++
程序(JAVA
,Python
,shell
都是可以的)叫做hello.cpp
// hello.cpp
#include
int main()
{
std::cout << "hello world" << std::endl;
return 0;
}
// test.c
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid == 0) {
// “./hello"是可执行文件的路径
// "hello"是执行的方式
execl("./hello", "hello", NULL);
exit(1);
} else {
// 进程等待,父进程回收子进程资源
int status = 0;
pid_t res = waitpid(pid, &status, 0);
if (res > 0) {
printf("wait child process success!\n");
if (WIFEXITED(status)) {
printf("exit code: %d\n", WEXITSTATUS(status));
} else {
printf("exit signal: %d\n", status & 0x7f);
}
}
}
return 0;
}
运行结果:
execlp()
int execlp(const char* file, const char* arg, ...);
file
:要替换的可执行程序的名字,会在环境变量PATH
中寻找该可执行程序的文件名, 就不用我们手动的写出可执行程序的路径了。arg
:给可执行程序传递的命令行参数(是一个可变参数列表),最后要以NULL
结尾。execl
执行成功,则进程替换成功,那么就没有返回值,因为进程已经执行其他进程了。如果execl
执行失败,则返回-1
。举例:
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid == 0) {
// 因为使用execlp会自动在PATH中寻找可执行程序的路径,所以直接写出可执行程序的名字即可
// "ls", "-a", "-l", "-i"还是是执行的方式
// 注意:第一个ls是可执行程序的名字,第二个ls是执行的方式
execlp("ls", "ls", "-a", "-l", "-i", NULL);
exit(1);
} else {
// 进程等待,父进程回收子进程资源
int status = 0;
pid_t res = waitpid(pid, &status, 0);
if (res > 0) {
printf("wait child process success!\n");
if (WIFEXITED(status)) {
printf("exit code: %d\n", WEXITSTATUS(status));
} else {
printf("exit signal: %d\n", status & 0x7f);
}
}
}
return 0;
}
运行结果:
execle()
int execle(const char* path, const char* arg, ..., char* const envp[]);
path
:要替换的可执行程序所在的路径arg
:给可执行程序传递的命令行参数(是一个可变参数列表),最后要以NULL
结尾。envp
:自己可以覆盖式地设置环境变量,达到让调用的程序使用自己设置的环境变量。execl
执行成功,则进程替换成功,那么就没有返回值,因为进程已经执行其他进程了。如果execl
执行失败,则返回-1
。举例:
我们先写一个程序runenv.c
,其中该程序中调用了环境变量。
// runenv.c
#include
#include
int main()
{
printf("my env:%s\n", getenv("MYENV")); // 打印出环境变量MYENV,这个在系统环境变量是没有的
printf("os env:%s\n", getenv("HOME")); // 打印出系统环境变量HOME
return 0;
}
// test.cpp
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid == 0) {
char* myenv[] = {
"MYENV=I am environment arg!", NULL};
// myenv中的环境变量覆盖了系统中的环境变量,runenv的程序可以调用覆盖后的环境变量
execle("./runenv", "runenv", NULL, myenv);
exit(1);
} else {
int status = 0;
pid_t res = waitpid(pid, &status, 0);
if (res > 0) {
printf("wait child process success!\n");
if (WIFEXITED(status)) {
printf("exit code: %d\n", WEXITSTATUS(status));
} else {
printf("exit signal: %d\n", status & 0x7f);
}
}
}
return 0;
}
运行结果:
注意:在系统中的环境变量HOME
是存在的,而使用了execle
之后,myenv
中的环境变量会覆盖系统中的环境变量,所以os env
变为(null)
了,而my env
中可以找到原来不存在于系统中的变量MYENV
。
execv()
int execv(const char* path, char* const argv[]);
path
:要替换的可执行程序所在的路径argv
:本质是一个指针数组,就是将在execl
中执行的方式以数组的方式储存起来。execl
执行成功,则进程替换成功,那么就没有返回值,因为进程已经执行其他进程了。如果execl
执行失败,则返回-1
。举例:
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid == 0) {
// 将执行方式写在一个数组中,然后传递一个指针数组给execv即可
char* myargv[] = {
"ls", "-a", "-l", "-i", NULL};
execv("/usr/bin/ls", myargv);
exit(1);
} else {
// 进程等待,父进程回收子进程资源
int status = 0;
pid_t res = waitpid(pid, &status, 0);
if (res > 0) {
printf("wait child process success!\n");
if (WIFEXITED(status)) {
printf("exit code: %d\n", WEXITSTATUS(status));
} else {
printf("exit signal: %d\n", status & 0x7f);
}
}
}
return 0;
}
运行结果:
execvp()
int execvp(const char* file, char* const argv[]);
file
:要替换的可执行程序的名字,会在环境变量PATH
中寻找该可执行程序的文件名, 就不用我们手动的写出可执行程序的路径了。argv
:本质是一个指针数组,就是将在execl
中执行的方式以数组的方式储存起来。execl
执行成功,则进程替换成功,那么就没有返回值,因为进程已经执行其他进程了。如果execl
执行失败,则返回-1
。举例:
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid == 0) {
// 将执行方式写在一个数组中,然后传递一个指针数组给execv即可
// 自动在PATH中会找可执行程序,所以不用直接写出可执行程序的完整路径
char* myargv[] = {
"ls", "-a", "-l", "-i", NULL};
execvp("ls", myargv);
exit(1);
} else {
// 进程等待,父进程回收子进程资源
int status = 0;
pid_t res = waitpid(pid, &status, 0);
if (res > 0) {
printf("wait child process success!\n");
if (WIFEXITED(status)) {
printf("exit code: %d\n", WEXITSTATUS(status));
} else {
printf("exit signal: %d\n", status & 0x7f);
}
}
}
return 0;
}
运行结果:
execve()
int execve(const char* path, char* const argv[], char* const envp[]);
path
:要替换的可执行程序所在的路径argv
:本质是一个指针数组,就是将在execl
中执行的方式以数组的方式储存起来。**envp
:自己可以覆盖式地设置环境变量,达到让调用的程序使用自己设置的环境变量。execl
执行成功,则进程替换成功,那么就没有返回值,因为进程已经执行其他进程了。如果execl
执行失败,则返回-1
。举例:
我们先写一个程序runenv.c
,其中该程序中调用了环境变量。
// runenv.c
#include
#include
int main()
{
printf("my env:%s\n", getenv("MYENV")); // 打印出环境变量MYENV,这个在系统环境变量是没有的
printf("os env:%s\n", getenv("HOME")); // 打印出系统环境变量HOME
return 0;
}
// test.cpp
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if (pid == 0) {
char* myargv[] = {
"runenv", NULL};
char* myenv[] = {
"MYENV=I am environment arg!", NULL};
// myenv中的环境变量覆盖了系统中的环境变量,runenv的程序可以调用覆盖后的环境变量
execle("./runenv", myargv, myenv);
exit(1);
} else {
int status = 0;
pid_t res = waitpid(pid, &status, 0);
if (res > 0) {
printf("wait child process success!\n");
if (WIFEXITED(status)) {
printf("exit code: %d\n", WEXITSTATUS(status));
} else {
printf("exit signal: %d\n", status & 0x7f);
}
}
}
return 0;
}
运行结果:
注意:在系统中的环境变量HOME
是存在的,而使用了execle
之后,myenv
中的环境变量会覆盖系统中的环境变量,所以os env
变为(null)
了,而my env
中可以找到原来不存在于系统中的变量MYENV
。
通过上面六个函数的学习,大家一定可以发现其中其实存在一些规律,他们的后缀函数如下:
l(list)
:表示参数采用列表的形式列出v(vector)
:表示参数采用数组的形式列出p(path)
:表示执行程序可以自动在环境变量PATH
中自动搜索可执行程序的路径e(env)
:表示自己可以传入自己写的环境变量函数名 | 参数格式 | 是否要写成绝对路径 | 是否使用当前的环境变量 |
---|---|---|---|
execl | 列表 | √ | √ |
execlp | 列表 | ×(文件名即可) | √ |
execle | 列表 | √ | ×(自己设置环境变量) |
execv | 数组 | √ | √ |
execvp | 数组 | ×(文件名即可) | √ |
execve | 数组 | √ | ×(自己设置环境变量) |
事实上:其实execve
才是真正的系统调用,其他的几个函数只不过对于execve
进行了封装。以满足不同的调用需求。
shell是一个命令行解释器,运行的原理很简单:当有命令需要执行的时候,shell创建一个子进程,然后让子进程去执行命令。
而创建子进程可以通过fork
完成,调用程序可以使用exec
函数完成,回收子进程可以通过wait
完成,而父进程只要控制这些进程即可。
执行步骤:
1.获取命令行参数(字符串处理)
2.解析命令行参数(分解字符串)
3.创建子进程(fork()
)
4.替换子进程(exec
系列函数)
5.等待子进程退出,并回收子进程资源和退出信息(wait()
或者waitpid()
)
#include
#include
#include
#include
#include
#define LEN 1024
#define MAX 32
int main()
{
char cmd[LEN];
char* myargv[MAX];
while (1) {
printf("[zhy@my_machine dir]$ ");
// 获取命令行参数
fgets(cmd, LEN, stdin);
// 解析字符串
cmd[strlen(cmd) - 1]= '\0';
myargv[0] = strtok(cmd, " ");
int i = 1;
while (myargv[i] = strtok(NULL, " "))
i ++;
// 创建子进程
pid_t pid = fork();
if (pid == 0) {
//child process
execvp(myargv[0], myargv);
exit(1);
} else {
// father process
int status = 0;
// 父进程阻塞式等待子进程
pid_t res = waitpid(pid, &status, 0);
if (res > 0) {
if (WIFEXITED(status)) {
printf("exit code: %d\n", WEXITSTATUS(status));
} else {
printf("exit signal: %d\n", status & 0x7f);
}
}
}
}
return 0;
}
运行结果:
注意:这只是一个简易版的shell,有很多的命令是不能执行的,但是在这一过程中,我们练习了如果使用fork
,exec
,wait
完成一系列的进程控制。
在一个c/c++
程序中,一个函数可以调用另一个函数,同时可以传递参数,并且函数会有返回值。
而进程中也有相似的体系结构。通过fork
,exec
,wait
,exit
来实现创建,调用,输出返回值和接收返回值。