本篇博客整理了进程控制有关的创建、退出、等待、替换操作方面的知识,最终附有模拟实现命令行解释器shell来综合运用进程控制的知识,旨在帮助读者更好地理解进程与进程之间的交互,以及对开发有一个初步了解。
目录
一、进程创建
(在【Linux系统】进程-CSDN博客中对本小节有更细致的总结和调用演示)
1.创建子进程 - fork()
在 Linux 中,fork()可以用于从已存在进程中创建一个新进程,这个新进程又称原进程的子进程,而原进程为父进程。
一个进程调用 fork()后,控制会转移到内核中的 fork(),在执行 fork()的代码的过程中,内核做了以下工作:
- 分配新的内存块和内核数据结构给子进程;
- 将父进程部分数据结构内容拷贝到子进程中;
- 添加子进程到系统进程列表当中;
- fork()返回,开始调度器调度。
当一个进程调用 fork() 后,就会生成一个和它有相同的二进制代码的进程,它们都在相同的地方运行,做着各自要做的工作。
【Tips】fork()的返回值:
- 对子进程返回0
- 对父进程返回子进程的 pid
- 创建失败或出错返回-1
【Tips】fork 调用失败的可能原因:
- 系统中有太多的进程
- 实际用户的进程数超过了限制
【Tips】fork()的使用情景——“父进程的助手”:
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段(例如,父进程等待客户端的请求,生成子进程来处理请求)。
- 一个进程要执行一个不同的程序(例如子进程从 fork()返回后,要调用 exec())。
2.写时拷贝
一般来说,父进程的代码是父子进程共享的,当父子进程不写入的时候,父进程的数据也是父子共享的,直到任意一方试图写入,操作系统就会以写时拷贝的方式为写入者按需生成一份以写入。
【Tips】操作系统是如何知道要进行写时拷贝的?
答案是:父进程在创建子进程的时候,操作系统会把父子进程页表中的数据项从读写权限设置成只读权限,此后父进程和子进程谁要对数据进行写入就一定会触发权限方面的问题,在进行权限审核的时候,操作系统会识别出来,历史上要访问的这个区域是可以被写入的,只不过暂时是只读状态,父子进程不管谁尝试对数据区进行写入的时候都会触发权限问题,但是针对这这种情况操作系统并不做异常处理,而是把数据拷贝一份,谁写的就把页表项进行重新映射,在拷贝完成后,就把只读标签重新设置成可读可写。
【Tips】操作系统为什么要采用写时拷贝?
父进程在创建子进程的时候,单纯的从技术角度去考虑,操作系统完全可以让父子进程共享同一份代码,然后把父进程的多有数据全部给子进程拷贝一份,技术上是完全可以实现的,但是操作系统为什么没有这样干?而是采用写时拷贝呢?原因主要有以下几点,首先假设父进程中国有100个数据,子进程只需要对其中的一个进行修改,剩下的99个子进程只读就可以,那如果操作系统把这100个数据全给子进程拷贝了一份,无疑是干了一件吃力不讨好的工作,全部拷贝既浪费了时间又浪费的物理内存,操作系统是绝对不会允许这种情况发生的,因此,对于数据段,操作系统采用的是写时拷贝的策略。
二、进程终止
【Tips】进程退出的三种情况:
- 代码运行完毕,结果正确。
- 代码运行完毕,结果不正确。
- 代码异常终止(进程崩溃)。
1.进程退出码
众所周知,主函数main()是程序的入口,但这句话其实并不准确,main()实际只是用户级别代码的入口,main()本身也是会被其他函数调用的,而其中有的函数是通过加载器被操作系统所调用的,也就是说,main()是间接性被操作系统所调用的。
既然main()是间接性被操作系统所调用的,那么当main()调用结束后,就应该给操作系统一个返回值。一段代码编译成功会生成一个可执行程序,这个可执行程序运行起来就变成了一个进程,当这个进程结束后,main()的返回值实际上就是这个进程的进程退出码。
每个进程退出码都有它对应的字符串含义,可以帮助用户确认程序执行成功或执行失败的原因,但它们具体表示的含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。一般来说,进程退出码有0或非0两种,0表示代码成功执行完毕,非0表示代码执行过程中出现错误。
一个可执行程序运行结束后,输入以下指令可以查看这个进程的进程退出码:
echo $?
【Tips】为什么进程退出码以0表示代码执行成功,以非0表示代码执行错误?
代码执行成功只有一种情况,成功了就是成功了,于是以0表示代码执行成功。
但代码执行错误却有多种原因,例如内存空间不足、非法访问、栈溢出等等,于是以非0的正数们来分别表示代码执行错误的原因。
【Tips】(C语言)strerror() - 获取该错误码对应的错误信息
2.正常退出一个进程
- main() 中 return
在 main() 中使用 return 语句退出进程,是最常见的方式,但只有在main()中的 return 语句才能起到退出进程的作用,其他子函数中的 return 语句只是传返回值,不能退出进程。
-
exit()
使用 exit() 退出进程也是一个常用的方法。exit() 可以在代码中的任何地方退出进程,且在退出进程前会做以下一系列工作:
- 执行用户通过atexit或on_exit定义的清理函数;
- 关闭所有打开的流,所有的缓存数据均被写入;
- 调用_exit() 终止进程。
exit() 在退出进程前会写入缓冲区中的数据。
-
_exit()
用_exit() 来退出进程并不常见。虽然_exit() 也可以在代码中的任何地方退出进程,但_exit() 会直接终止进程,不会在退出进程前会做任何收尾处理。
使用_exit() 终止进程,缓冲区中的数据将不会被输出。
【Tips】return 与 exit()
main()的调用结束后,系统会将main()的返回值当做exit()的参数去调用exit()来退出进程,所以其实执行 “return num;” 等同于执行 “exit(num);”。
【Tips】exit 与 _exit
exit() 退出进程前,会执行用户定义的清理函数、冲刷缓冲、关闭流等操作,然后再终止进程。
_exit() 会直接终止进程,不会做任何收尾处理。
其实,_exit() 是系统调用接口,exit() 是库函数,exit() 中封装了 _exit()。但在exit() 内部调用 _exit()退出进程之前,会先执行用户通过 atexit 或 on_exit 定义的清理函数、关闭所有打开的流、写入所有的缓冲区数据。
3.进程的异常退出
进程如果因异常而退出,那么它的代码可能还没有跑完,可能还没有执行 return 语句,所以,如果一个进程出现了异常,那么它的退出码就没有意义了。
对于一个执行结束的进程来说,应该先看它是否出现异常,如果没有出现异常,就再去看它的退出码是否正确。
进程出现异常,本质上是进程收到了某些信号,例如代码中的除0错误、空指针解引用等,一般都会引发硬件错误,此时,操作系统就会向对应的进程发送信号。
【Tips】程序异常的可能情况:
- 向进程发送信号导致进程异常退出。例如,在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。
- 代码错误导致进程运行时异常退出。例如,代码当中存在野指针问题使得进程运行时异常退出,或是出现除0的情况使得进程运行时异常退出等。
三、进程等待
进程等待其实就是在父进程的代码中,通过系统调用 wait() 或 waitpid(),来完成对子进程进行状态的检测与子进程退出信息的回收。
【Tips】进程等待的必要性
- 子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。
- 进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程。
- 对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何。
- 父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
【小结】
- 通过进程等待,可以“杀死”僵尸进程,防止内存泄露,更好地维护内存资源。
- 进程等待为父进程提供了是否了解子进程退出情况的选择余地,对于子进程协助自己完成任务的情况,父进程可以关心也可以不关心。
1.wait() 和 waitpid()
- wait()
头文件:
1、 #include<sys/wait.h>//这是wait函数的头文件
2、 #include<sys/type.h>//这是pid_t的类型的头文件
函数声明:
pid_t wait(int*status);
返回值:等待成功则返回所等待进程的pid,失败则返回-1
参数:status是一个输出型参数,实际是一个保存程序异常信号和退出码的位图
- 功能:等待任意子进程。
- 返回值:等待成功返回被等待进程的pid,等待失败返回-1。
- 参数:用于保存子进程的退出信息,不关心可设为NULL。
-------------------------------
- waitpid()
头文件:
#include<sys/types.h>
#include<sys/wait.h>
函数声明:
pid_t waitpid(pid_t id,int *status,int option);
参数:
1.id
情况1: 等于-1,等待任意一个子进程。
情况2: 大于0,等待与id值相等的子进程。
2.status
情况1:为空,不关心子进程的退出信息。
情况2:非空,将退出信息写入status指向的位图中。
3.option
情况1:0,表示阻塞等待。
情况2:WNOHANG,表示非阻塞轮询,若子进程没有退出则立即返回。
//...
返回值:
情况1:参数 option 为 0,等待成功则返回子进程的pid,失败则返回-1。
情况2:参数 option 为 WOHANG,等待成功则返回子进程的pid,子进程没有则退出返回0,失败则返回-1。
获取 status 保存的信息————
判断是否正常退出: WIFEXITED(status)
获取退出状态: WEXITSTATUS(status)
判断是否被信号终止: WIFSIGNALED(status)
获取信号信息: WTERMSIG(status)
- 功能:等待指定子进程或任意子进程。
- 返回值:参数 options 设置成0时,等待成功则返回所等待的子进程的 pid;参数 options 设置成 WNOHANG 时,且调用时没有子进程退出,等待成功则返回0;如果调用时出错,则返回 -1,此时 errno 会被设置成相应的值以指示错误所在。
- 参数 pid:pid = -1 表示等待任意一个子进程,与 wait() 等效;pid > 0 表示等待相应 pid 的子进程。
- 参数 status:是一个保存了异常信号和退出码的位图,可以通过WIFEXITED(status) 来查看子进程是否正常退出,若为正常终止子进程返回的状态,则为真值;还可以通过WEXITSTATUS(status) 来查看子进程的退出码,若非0,则提取子进程的退出码。
- 参数 options:0 表示父进程阻塞式等待,即子进程如果处在其它状态且不处在Z状态,父进程就会变成 S 状态,此时,操作系统会把父进程放到子进程 PCB 对象中的等待队列,以阻塞的方式等待子进程变成僵尸状态,直到子进程运行结束,把父进程重新唤醒,让父进程回收子进程;WNOHANG 表示非阻塞轮询等待,若参数 pid 指定的子进程没有结束且处于其它状态,则 waitpid() 返回0,不予等待,若正常结束,则返回该子进程的 pid。
------------------------
【ps】wait() 和 waitpid() 都只能等待它们所处的进程的子进程,如果等待了其它进程就会出错。
【Tips】子进程的退出信息都被保存在wait() 或 waitpid() 的参数 status 中——
- wait() 和 waitpid() 都有一个 status 参数,该参数是一个输出型参数,由操作系统来填充。
- 只有向 status 传 NULL,表示父进程不关心子进程的退出情况;否则,操作系统会根据 status ,将子进程的退出信息反馈给父进程。
- status 不是一个简单的整型指针,其实是一个位图,它的16个低比特位与进程的退出信息有关,其中,0到7位保存了进程收到的异常信号(特别的,第7位是core dump标志),8到15位保存了进程的退出码。
【Tips】wait() 和 waitpid() 的实现原理
一个进程在自身退出之后、被父进程回收之前,它的代码和数据都被操作系统释放了,但它保存异常信号和退出码的PCB对象还留在内存中。wait() 和 waitpid() 本质上就是通过操作系统去检查这个进程是否处于僵尸状态(Z状态),如果它处于僵尸状态,就去它的PCB对象中拿到它所收的信号和退出码,然后把这些信息赋值给 wait() 和 waitpid() 的参数 status,并将这个进程的状态置为死亡状态(X状态)。
由于PCB对象属于内核数据结构,用户是无法直接访问的,因此这个工作必须全程由操作系统来完成。
【Tips】一个进程不仅可以等待硬件资源,也可以等待软件资源。
2.阻塞等待
如果子进程不退出,父进程就一直等待而停下手里的活儿,直到子进程退出才继续做自己的任务,这种等待方式被称为阻塞等待。
2.1-父进程只等待一个进程
- 创建子进程后,父进程可使用 wait() 一直等待子进程,直到子进程退出后读取子进程的退出信息。
为方便演示,此处引入以下代码:
//proc.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0){
//child
int count = 10;
while (count--){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t ret = wait(&status);
if (ret >= 0){
//wait success
printf("wait child success...\n");
}
else {
//wait fail
printf("wait child fail...\n");
}
sleep(3);
return 0;
}
以及监控进程的脚本:
while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done
以上代码中,子进程会在执行完10次while循环后退出,而父进程会一直等待子进程退出。若wait()返回了子进程的pid,则说明等待成功,若返回了-1,则等待失败。
由演示图,当子进程退出后,父进程及时回收了子进程的退出信息,子进程也就不会变成僵尸进程了。
- 创建子进程后,父进程可使用waitpid()一直等待子进程(此处需将 waitpid() 的第三个参数 options 设为0),直到子进程退出后读取子进程的退出信息。
为方便演示,此处引入以下代码:
//proc.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0){
//child
int count = 10;
while (count--){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret >= 0){
//wait success
printf("wait child success...\n");
if (WIFEXITED(status)){
//exit normal
printf("exit code:%d\n", WEXITSTATUS(status));
}
else{
//signal killed
printf("killed by siganl %d\n", status & 0x7F);
}
}
sleep(3);
return 0;
}
以上代码中,子进程会在执行完10次while循环后退出,而父进程会一直等待子进程退出。若waitpid()返回了子进程的pid,则说明等待成功,若返回了-1,则等待失败。
2.2-父进程等待多个子进程
上文中已提到,父进程可以调用 wait() 或 waitpid() 来等待一个子进程退出。
其实,父进程不仅可以等待一个子进程退出,还可以等待多个子进程退出,而等待多个子进程退出,一般通过调用 waitpid() 来完成。
为方便演示,此处引入以下代码:
//proc.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t ids[10];
int i = 0;
for (; i < 10; i++){
pid_t id = fork();
if (id == 0){
//child
printf("child process created successfully...PID:%d\n", getpid());
sleep(3);
exit(i); //将子进程的退出码设置为该子进程PID在数组ids中的下标
}
//father
ids[i] = id;
}
for (i = 0; i < 10; i++){
int status = 0;
pid_t ret = waitpid(ids[i], &status, 0);
if (ret >= 0){
//wait child success
printf("wait child success..PID:%d\n", ids[i]);
if (WIFEXITED(status)){
//exit normal
printf("exit code:%d\n", WEXITSTATUS(status));
}
else{
//signal killed
printf("killed by signal %d\n", status & 0x7F);
}
}
}
return 0;
}
以上代码中,通过for循环同时创建了10个子进程,同时将子进程的pid放入到ids数组当中,并将这10个子进程退出时的退出码设置为该子进程pid在数组ids中的下标,之后父进程再调用 waitpid() 指定等待这10个子进程。
3.非阻塞轮询
当子进程未退出时,父进程可以做一些自己的工作,不必一直干等,直到子进程退出时再去回收子进程的退出信息,这种等待方式就叫做非阻塞等待。具体的方式是,父进程每隔一段时间会去查看子进程是否退出,如果没有退出,就先去忙自己的活儿,如果退出了,就回收子进程的退出信息,因此,非阻塞等待又称非阻塞轮询。
一般通过调用 waitpid() 并将第三个参数 options 设为 WNOHANG 来实现非阻塞轮询。
为方便演示,此处引入以下代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0){
//child
int count = 3;
while (count--){
printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(3);
}
exit(0);
}
//father
while (1){
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret > 0){
printf("wait child success...\n");
printf("exit code:%d\n", WEXITSTATUS(status));
break;
}
else if (ret == 0){
printf("father do other things...\n");
sleep(1);
}
else{
printf("waitpid error...\n");
break;
}
}
return 0;
}
以上代码中,子进程执行三次while循环便退出,父进程在等待子进程退出。当 waitpid() 返回 0 ,非阻塞轮询继续,父子进程都在做各自的工作;当 waitpid() 返回一个正数(子进程的pid),说明子进程退出了,此时父进程就去回收子进程的退出信息。
四、进程的程序替换
//command.c
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("before: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("after: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());
return 0;
}
在以下代码中,execl() 是一个库函数,功能是进行进程的替换。按理来说,这份代码生成的程序会先打印 “before: ...”,再打印 “after: ...” 。但实际并不如此:
这个程序在调用了 execl() 后,去执行了 ls 指令(也是一个可执行程序),进程中原本要打印的 “after: ...” 并没有打印。这个现象就是进程的程序替换。
类似于夺舍题材的网络小说——曾经有一位盖世大能,因为一些原因轮回降世,夺舍了一个出身普通能力平庸的路人甲,从此走上了扮猪吃老虎的爽文之路——盖世大能夺舍的是路人甲的灵魂,路人甲的肉体是没变,所以在旁人看来,虽然路人甲还是那个路人甲,但他不知怎么地就突然功力盖世了;进程替换也是如此,替换的是一个进程的代码和数据,但这个进程的本体是没变的,对用户来说,这个进程还是这个进程,还保有它原本的名字、PCB对象、属性、路径等等等等,只是它的代码和数据变了,突然可以执行其他功能了。
它的使用情景例如,用 fork() 创建子进程后,子进程默认执行的是和父进程几乎相同的程序(有可能执行不同的代码分支),但如果想让子进程做别的工作,去执行别的程序,就离不开进程替换。
1.基本原理
一个进程要进行程序替换,一般是通过在进程中调用 exec() 系列的接口来完成的,当exec() 系列被这个进程调用时,这个进程的代码和数据会被新程序全部替换,并从新程序的启动例程开始执行。
【Tips】发生程序替换时,并没有新的进程被创建
一个进程经历程序替换后,它的PCB、进程地址空间和页表等数据结构都没有发生改变,只是它在物理内存当中的数据和代码因替换而改变了,这个过程并没有创建新的进程,进程在经历程序替换之前和之后的进程pid是一样的。
【Tips】子进程经历程序替换后,并不会影响父进程的代码和数据
子进程刚被创建时,与父进程共享代码和数据,但当子进程要经历程序替换时,也就意味着子进程要对共享的代码和数据进行写入操作,这时操作系统会单独为子进程分配一份内存资源,协助子进程进行写时拷贝,使得父子进程的代码和数据分离。因此,子进程经历程序替换后,并不会影响父进程的代码和数据。
【补】由于exec() 系列,在替换成功时不会返回,只会在替换失败时返回,因此,程序替换成功的时候,exec() 系列所在语句之后的代码不会被执行,只有替换失败的时候,之后的代码才有可能被执行。
【补】在 bash 中的所有进行其实都算是 bash 的子进程,且都是通过 exec 系列接口将程序对应的代码和数据加载到内存中的。因此, exev 系列接口起到了加载器的效果,接口里面也一定会涉及到内存申请、外设访问等操作。
2.exec 系列接口
功能与程序替换相关的接口一共有七个,除了有一个是系统调用,其余六个都是底层封装了这个系统调用的库函数。
//系统调用
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
//库函数
#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 execvpe(const char *file, char *const argv[], char *const envp[]);
2.1-execl()
头文件:
#include<unistd.h>
函数声明:
int execl(const char *path, const char *arg, ...);
参数:
1.path:指的是所要打开文件具体的路径(分为绝对路径和相对路径),
2.arg: 指的是所要打开的文件名。
3. ...:可变参数列表,传的是具体的选项,且(标准写法)最后一个必须以NULL结尾。
返回值:
情况1:如果替换成功,则直接按照替换之后的代码往下执行,原先的代码不执行。
情况2:替换失败,返回-1,按照原先的代码往下继续运行,并且错误码将被设置。
路径有效,一般会替换成功。
路径无效,则替换失败。
2.2-execlp()
头文件:
#include<unistd.h>
函数声明:
int execlp(const char *file, const char *arg, ...);
参数:
1.file:指的是所要打开文件的路径(分为绝对路径和相对路径),如果不加绝对路径
默认在相对路径与path环境变量下找。
2.arg: 指的是所要打开的文件名。
3. ...:可变参数列表,传的是具体的选项,且(标准写法)最后一个必须以NULL结尾。
返回值:
情况1:如果替换成功,则直接按照替换之后的代码往下执行,原先的代码不执行。
情况2:替换失败,返回-1,按照原先的代码往下继续运行,并且错误码将被设置。
2.3-execvp()
#include<unistd.h>
函数声明:
int execlvp(const char *file, char *const argv[]);
参数:
1.file:指的是所要打开文件的路径(分为绝对路径和相对路径),如果不加绝对路径
默认在相对路径与path环境变量下找。
2.argv: 指的是所要打开的文件名及其选项,且其中的指针的指向不能被更改。
返回值:
情况1:如果替换成功,则直接按照替换之后的代码往下执行,原先的代码不执行。
情况2:替换失败,返回-1,按照原先的代码往下继续运行,并且错误码将被设置。
2.4-execvpe()
#include<unistd.h>
函数声明:
int execvpe(const char *file, char *const argv[]);
参数:
1.file:指的是所要打开文件的路径(分为绝对路径和相对路径),如果不加绝对路径
默认在相对路径与path环境变量下找。
2.argv: 指的是所要打开的文件名及其选项,且其中的指针的指向不能被更改。
返回值:
情况1:如果替换成功,则直接按照替换之后的代码往下执行,原先的代码不执行。
情况2:替换失败,返回-1,按照原先的代码往下继续运行,并且错误码将被设置。
通过execvpe(),可以直接向子进程导入用户自定义的环境变量。
2.5-命名区分
//系统调用
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
//库函数
#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 execvpe(const char *file, char *const argv[], char *const envp[]);
系统调用 execve 在man手册的第2节,其它六个库函数在man手册的第3节。exec 系列的六个库函数能够适配不同的调用场景,但都在底层封装了系统调用 execve 。
exec 系列的库函数,函数名都以 exec 开头,而它们后缀的含义如下:
- l (list):表示参数采用列表的形式,一一列出。
- v (vector):表示参数采用数组的形式。
- p (path):表示能自动搜索环境变量PATH,进行程序查找。
- e (env):表示可以传入自己设置的环境变量。
库函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 否 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 否 | 否,需自己组装环境变量 |
execv | 数组 | 否 | 是 |
execvp | 数组 | 是 | 是 |
execvpe | 数组 | 是 | 否,需自己组装环境变量 |
2.6-统一理解
exec 系列的六个库函数命名后缀不同,功能存在区别,但它们的参数有一些共性:
- 都拥有的第一个参数——负责找到新替换进来的程序:方案一,函数名不带 p ,第一个形参 path 表示可执行程序的全路径;方案二,函数名带 p 的,第一个形参 file 表示可执行程序名,帮助函数拿着这个程序名去环境变量 PATH 下找到这个程序。
- 都拥有的第二个参数——负责执行新替换的程序:方案一,函数名带 l ,表示参数采用列表,通过可变参数的形式接收指令和选项(命令行中输入什么,这里就传什么),但列表最后要以 NULL 结尾;方案二,函数名带 v ,表示参数采用字符指针数组,把指令和选项都存到一个字符指针数组中(数组结尾必须是 NULL),然后把这个数组作为实参去传给程序替换函数,这个实参最终会作为命令行参数传递给新替换进来的可执行程序。
- 可能拥有的第三个参数——采用自定义的环境变量:函数名带 e,参数中就有一个环境变量表 envp(也是字符指针数组),新替换进来的进程不再继承父进程的环境变量,而会通过覆盖的方式,将 envp 数组的环境变量作为自己的环境变量,彻底替换掉父进程的环境变量。
补、模拟实现shell
shell 和 bash 一样,也是一种命令行解释器,它的运行原理基本为:当有命令需要执行时,shell 会创建子进程,让子进程去执行命令,而shell只需等待子进程退出即可。所以,shell 的执行逻辑其实很简单,只需循环执行以下步骤即可:
- 获取命令行;
- 解析命令行;
- 创建子进程;
- 替换子进程;
- 等待子进程退出。
而创建子进程可以使用 fork(),替换子进程可以使用 exec 系列函数,等待子进程可以使用 wait() 或 waitpid() ......具体代码如下:
//MyShell.c
#include <stdio.h>
#include <pwd.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#define LEN 1024 //命令最大长度
#define NUM 32 //命令拆分后的最大个数
int main()
{
char cmd[LEN]; //存储命令
char* myargv[NUM]; //存储命令拆分后的结果
char hostname[32]; //主机名
char pwd[128]; //当前目录
while (1){
//获取命令提示信息
struct passwd* pass = getpwuid(getuid());
gethostname(hostname, sizeof(hostname)-1);
getcwd(pwd, sizeof(pwd)-1);
int len = strlen(pwd);
char* p = pwd + len - 1;
while (*p != '/'){
p--;
}
p++;
//打印命令提示信息
printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);
//读取命令
fgets(cmd, LEN, stdin);
cmd[strlen(cmd) - 1] = '\0';
//拆分命令
myargv[0] = strtok(cmd, " ");
int i = 1;
while (myargv[i] = strtok(NULL, " ")){
i++;
}
pid_t id = fork(); //创建子进程执行命令
if (id == 0){
//child
execvp(myargv[0], myargv); //child进行程序替换
exit(1); //替换失败的退出码设置为1
}
//shell
int status = 0;
pid_t ret = waitpid(id, &status, 0); //shell等待child退出
if (ret > 0){
printf("exit code:%d\n", WEXITSTATUS(status)); //打印child的退出码
}
}
return 0;
}
【ps】当执行由 Myshell.c 生成的可执行程序 ./MyShell,就是模拟实现的 MyShell 在进行命令行解释了。MyShell 被设计成,会在子进程退出后打印子进程的退出码,可以根据这一点来对模拟实现的 MyShell 和 Linux下的命令行解释器做区分。