中国建设银行网站会员注册,中国网站建设公司前十名,wordpress数据库名怎么修改,制作宣传片的步骤本篇博客整理了进程控制有关的创建、退出、等待、替换操作方面的知识#xff0c;最终附有模拟实现命令行解释器shell来综合运用进程控制的知识#xff0c;旨在帮助读者更好地理解进程与进程之间的交互#xff0c;以及对开发有一个初步了解。
目录
一、进程创建
1.创建子进… 本篇博客整理了进程控制有关的创建、退出、等待、替换操作方面的知识最终附有模拟实现命令行解释器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信号使得进程异常退出或是使用CtrlC使得进程异常退出等。代码错误导致进程运行时异常退出。例如代码当中存在野指针问题使得进程运行时异常退出或是出现除0的情况使得进程运行时异常退出等。 三、进程等待 进程等待其实就是在父进程的代码中通过系统调用 wait() 或 waitpid()来完成对子进程进行状态的检测与子进程退出信息的回收。 【Tips】进程等待的必要性 子进程退出父进程如果不读取子进程的退出信息子进程就会变成僵尸进程进而造成内存泄漏。进程一旦变成僵尸进程那么就算是kill -9命令也无法将其杀死因为谁也无法杀死一个已经死去的进程。对于一个进程来说最关心自己的就是其父进程因为父进程需要知道自己派给子进程的任务完成的如何。父进程需要通过进程等待的方式回收子进程资源获取子进程的退出信息。 【小结】 通过进程等待可以“杀死”僵尸进程防止内存泄露更好地维护内存资源。进程等待为父进程提供了是否了解子进程退出情况的选择余地对于子进程协助自己完成任务的情况父进程可以关心也可以不关心。 1.wait() 和 waitpid()
wait() 头文件:
1、 #includesys/wait.h//这是wait函数的头文件
2、 #includesys/type.h//这是pid_t的类型的头文件函数声明
pid_t wait(int*status);返回值等待成功则返回所等待进程的pid失败则返回-1参数status是一个输出型参数实际是一个保存程序异常信号和退出码的位图
功能等待任意子进程。返回值等待成功返回被等待进程的pid等待失败返回-1。参数用于保存子进程的退出信息不关心可设为NULL。 -------------------------------
waitpid() 头文件:
#includesys/types.h
#includesys/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 会被设置成相应的值以指示错误所在。参数 pidpid -1 表示等待任意一个子进程与 wait() 等效pid 0 表示等待相应 pid 的子进程。参数 status是一个保存了异常信号和退出码的位图可以通过WIFEXITED(status) 来查看子进程是否正常退出若为正常终止子进程返回的状态则为真值还可以通过WEXITSTATUS(status) 来查看子进程的退出码若非0则提取子进程的退出码。参数 options0 表示父进程阻塞式等待即子进程如果处在其它状态且不处在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.hint 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()
头文件:
#includeunistd.h
函数声明:
int execl(const char *path, const char *arg, ...);
参数:
1.path:指的是所要打开文件具体的路径分为绝对路径和相对路径
2.arg: 指的是所要打开的文件名。
3. ...:可变参数列表,传的是具体的选项且标准写法最后一个必须以NULL结尾。
返回值:
情况1:如果替换成功则直接按照替换之后的代码往下执行原先的代码不执行。
情况2:替换失败,返回-1按照原先的代码往下继续运行并且错误码将被设置。 路径有效一般会替换成功。 路径无效则替换失败。 2.2-execlp()
头文件:
#includeunistd.h
函数声明:
int execlp(const char *file, const char *arg, ...);
参数:
1.file:指的是所要打开文件的路径分为绝对路径和相对路径,如果不加绝对路径默认在相对路径与path环境变量下找。
2.arg: 指的是所要打开的文件名。
3. ...:可变参数列表,传的是具体的选项且标准写法最后一个必须以NULL结尾。
返回值:
情况1:如果替换成功则直接按照替换之后的代码往下执行原先的代码不执行。
情况2:替换失败,返回-1按照原先的代码往下继续运行并且错误码将被设置。 2.3-execvp()
#includeunistd.h
函数声明:
int execlvp(const char *file, char *const argv[]);参数:
1.file:指的是所要打开文件的路径分为绝对路径和相对路径,如果不加绝对路径默认在相对路径与path环境变量下找。
2.argv: 指的是所要打开的文件名及其选项且其中的指针的指向不能被更改。返回值:
情况1:如果替换成功则直接按照替换之后的代码往下执行原先的代码不执行。
情况2:替换失败,返回-1按照原先的代码往下继续运行并且错误码将被设置。2.4-execvpe()
#includeunistd.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下的命令行解释器做区分。