【Linux系统】进程控制
本篇博客整理了进程控制有关的创建、退出、等待、替换操作方面的知识,最终附有模拟实现命令行解释器shell来综合运用进程控制的知识,旨在帮助读者更好地理解进程与进程之间的交互,以及对开发有一个初步了解。
目录
一、进程创建
1.创建子进程 - fork()
2.写时拷贝
二、进程终止
1.进程退出码
2.正常退出一个进程
3.进程的异常退出
三、进程等待
1.wait() 和 waitpid()
2.阻塞等待
2.1-父进程只等待一个进程
2.2-父进程等待多个子进程
3.非阻塞轮询
四、进程的程序替换
1.基本原理
2.exec 系列接口
2.1-execl()
2.2-execlp()
2.3-execvp()
2.4-execvpe()
2.5-命名区分
2.6-统一理解
补、模拟实现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位保存了进程收到的异常信号,第8位是core dump标志,9到16位保存了进程的退出码。
【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){//childprintf("child process created successfully...PID:%d\n", getpid());sleep(3);exit(i); //将子进程的退出码设置为该子进程PID在数组ids中的下标}//fatherids[i] = id;}for (i = 0; i < 10; i++){int status = 0;pid_t ret = waitpid(ids[i], &status, 0);if (ret >= 0){//wait child successprintf("wait child success..PID:%d\n", ids[i]);if (WIFEXITED(status)){//exit normalprintf("exit code:%d\n", WEXITSTATUS(status));}else{//signal killedprintf("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){//childint count = 3;while (count--){printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());sleep(3);}exit(0);}//fatherwhile (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){//childexecvp(myargv[0], myargv); //child进行程序替换exit(1); //替换失败的退出码设置为1}//shellint 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下的命令行解释器做区分。
相关文章:

【Linux系统】进程控制
本篇博客整理了进程控制有关的创建、退出、等待、替换操作方面的知识,最终附有模拟实现命令行解释器shell来综合运用进程控制的知识,旨在帮助读者更好地理解进程与进程之间的交互,以及对开发有一个初步了解。 目录 一、进程创建 1.创建子进…...
Go语言数值类型教程
Go语言提供了丰富的数值类型,包括整数类型、浮点类型和复数类型。每种类型都有其特定的用途和存储范围。下面将详细介绍这些类型,并附带示例代码。 原文链接: Go语言数值类型教程 - 红客网-网络安全与渗透技术 1. 整数类型 原文链接…...

Linux进程控制——Linux进程等待
前言:接着前面进程终止,话不多说我们进入Linux进程等待的学习,如果你还不了解进程终止建议先了解: Linux进程终止 本篇主要内容: 什么是进程等待 为什么要进行进程等待 如何进程等待 进程等待 1. 进程等待的概念2. 进…...

GPT-4o:融合文本、音频和图像的全方位人机交互体验
引言: GPT-4o(“o”代表“omni”)的问世标志着人机交互领域的一次重要突破。它不仅接受文本、音频和图像的任意组合作为输入,还能生成文本、音频和图像输出的任意组合。这一全新的模型不仅在响应速度上达到了惊人的水平,在文本、音频和图像理解方面也表现出色,给人带来了…...

灵活的静态存储控制器 (FSMC)的介绍(STM32F4)
目录 概述 1 认识FSMC 1.1 应用介绍 1.2 FSMC的主要功能 1.2.1 FSMC用途 1.2.2 FSMC的功能 2 FSMC的框架结构 2.1 AHB 接口 2.1.1 AHB 接口的Fault 2.1.2 支持的存储器和事务 2.2 外部器件地址映射 3 地址映射 3.1 NOR/PSRAM地址映射 3.2 NAND/PC卡地址映射 概述…...
nginx-rtmp
1.已经安装nginx;configure配置模块;make编译无需安装;把objs/nginx复制到已安装的宁目录下 ./configure --prefix/usr/local/nginx --add-module/usr/local/src/fastdfs-nginx-module/src --add-module/usr/local/src/nginx-rtmp-module-mas…...
nginx 代理java 请求报502
情况:nginx代理java 请求 后端返回正常,但是经过nginx 时报502 经过多次对比其他接口发现可能是返回的请求头过大,导致nginx 报错:如下 2024/05/13 02:57:12 [error] 88#88: *3755 upstream sent too big header while reading r…...

面试集中营—Redis面试题
一、Redis的线程模型 Redis是基于非阻塞的IO复用模型,内部使用文件事件处理器(file event handler),这个文件事件处理器是单线程的,所以Redis才叫做单线程的模型,它采用IO多路复用机制同时监听多个socket&a…...

关于使用git拉取gitlab仓库的步骤(解决公钥问题和pytho版本和repo版本不对应的问题)
先获取权限,提交ssh-key 虚拟机连接 GitLab并提交代码_gitlab提交mr-CSDN博客 配置完成上诉步骤之后,执行下列指令进行拉去仓库的内容 sudo apt install repo export PATHpwd/.repo/repo:$PATH python3 "实际路径"/repo init -u ssh://gitxx…...

Django图书馆综合项目-学习(2)
接下来我们来实现一下图书管理系统的一些相关功能 1.在书籍的book_index.html中有一个"查看所有书毂"的超链接按钮,点击进入书籍列表book_list.html页面. 这边我们使用之前创建的命名空间去创建超连接 这里的book 是在根路由创建的namespacelist是在bo…...
vue3+ts 获取input 输入框中的值
从前端input 输入框获取值,通过封装axios 将值传给后端服务 数据格式为json html <el-form> <el-form-item label"域名"><el-input v-model"short_url" style"width: 240px"type"text"placeholder&quo…...
Gin框架返回Protobuf类型:提升性能的利器
在构建高效、高性能的微服务架构时,数据序列化和反序列化的性能至关重要。Protocol Buffers(简称Protobuf)作为一种轻量级且高效的结构化数据存储格式,已经在众多领域得到广泛应用。Gin框架作为Go语言中流行的Web框架,…...

HTML满屏漂浮爱心
目录 写在前面 满屏爱心 代码分析 系列推荐 写在最后 写在前面 小编给大家准备了满屏漂浮爱心代码,一起来看看吧~ 满屏爱心 文件heart.svg <svg xmlns"http://www.w3.org/2000/svg" width"473.8px" height"408.6px" view…...
爬虫应该选择住宅ip代理还是数据中心代理?
住宅代理 住宅代理是互联网服务提供商 (ISP) 提供的 IP 地址,它们是附加到实际物理位置的真实IP地址。住宅代理允许用户通过目标区域内的真实IP地址连接到互联网。 数据中心代理 数据中心代理是指是使用数据中心拥有并管理IP的代理,IP地址来源于数据中…...
百面算法工程师目录 | 深度学习目标检测、语义分割、分类上百种面试问答技巧
本文给大家带来的百面算法工程师是深度学习面试目录大纲,文章内总结了常见的提问问题,旨在为广大学子模拟出更贴合实际的面试问答场景。在这篇文章中,可以点击题目直达问题答案处,方便查找问题寻找答案。节约大家的时间。通过对这…...

Java中Maven的依赖管理
依赖介绍 是指当前项目运行所需要的jar包,一个项目中可以引入多个依赖 配置 在pom.xml中编写<dependencies>标签 在<dependencies>中使用<dependency>引入标签 定义坐标的groupId、rtifactId、version 点击刷新按钮、引入新坐标 例如引入下…...

Github新手入门使用方法
**存在问题:**新手如何快速入门github,能够下载开源文件,并且修改后更新远程github仓库; 解决方案: 参考: http://www.360doc.com/content/24/0301/12/60419_1115656653.shtml https://blog.csdn.net/gongd…...

期权隐含波动率到底是什么意思?
今天期权懂带你了解期权隐含波动率到底是什么意思?期权隐含波动率解析。通俗的说,期权隐含波动率是在期权市场中买家和卖家对于,某一期权合约价格变动幅度大小的判断。 期权隐含波动率到底是什么意思? 隐含波动率是根据期权市场价…...
28、Flink 为管理状态自定义序列化
为管理状态自定义序列化 a)概述 对状态使用自定义序列化,包含如何提供自定义状态序列化程序、实现允许状态模式演变的序列化程序。 b)使用自定义状态序列化程序 注册托管 operator 或 keyed 状态时,需要 StateDescriptor 来指…...

【强训笔记】day17
NO.1 思路:用一个字符串实现,stoi函数可以转化为数字并且去除前导0。 代码实现: #include <iostream> #include<string> using namespace std;string s;int main() {cin>>s;for(int i0;i<s.size();i){if(s[i]%20) s[…...

Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
<6>-MySQL表的增删查改
目录 一,create(创建表) 二,retrieve(查询表) 1,select列 2,where条件 三,update(更新表) 四,delete(删除表…...

Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...
可靠性+灵活性:电力载波技术在楼宇自控中的核心价值
可靠性灵活性:电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中,电力载波技术(PLC)凭借其独特的优势,正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据,无需额外布…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LA…...
06 Deep learning神经网络编程基础 激活函数 --吴恩达
深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...

华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建
华为云FlexusDeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色,华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型,能助力我们轻松驾驭 DeepSeek-V3/R1,本文中将分享如何…...

AI,如何重构理解、匹配与决策?
AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...
Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?
Redis 的发布订阅(Pub/Sub)模式与专业的 MQ(Message Queue)如 Kafka、RabbitMQ 进行比较,核心的权衡点在于:简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...

算法:模拟
1.替换所有的问号 1576. 替换所有的问号 - 力扣(LeetCode) 遍历字符串:通过外层循环逐一检查每个字符。遇到 ? 时处理: 内层循环遍历小写字母(a 到 z)。对每个字母检查是否满足: 与…...