当前位置: 首页 > news >正文

公司网站页面设计网络运营方案怎么写

公司网站页面设计,网络运营方案怎么写,曲阳有没有做网站里,个人静态网页制作教程目录 1. 进程创建 1.1. 内核数据结构的处理 1.2. 代码的处理 1.3. 数据的处理#xff1a; 方案一#xff1a;fork创建子进程的时候#xff0c;直接对数据进行拷贝处理#xff0c;让父子进程各自私有一份 方案二#xff1a;写实拷贝(copy on write) 1.4. fork常规用…目录 1. 进程创建 1.1. 内核数据结构的处理 1.2. 代码的处理 1.3. 数据的处理 方案一fork创建子进程的时候直接对数据进行拷贝处理让父子进程各自私有一份 方案二写实拷贝(copy on write) 1.4. fork常规用法 1.5.fork失败的原因 1.6. 扩展 2. 进程终止 2.1. 进程退出场景 2.2. 什么是退出码  2.3. 退出码如何表现的  2.4. 退出码的意义 2.5. 退出码的演示 2.5.1. 代码跑完进程正常终止结果正确 2.5.2. 代码跑完进程正常终止结果不正确 2.5.3. 代码异常终止 2.6. 如何正常的终止一个进程 2.6.1. main里面的return 2.6.2. exit 2.6.3. _exit 2.6.4. _exit和exit的一个区别 3. 进程等待 进程等待是什么 进程等待为什么 3.1. wait等待子进程 3.2. waitpid等待子进程 3.3. 如何获取子进程的exit code 3.4. 如何获取子进程的退出信号 3.5. 用操作系统提供的宏帮助我们获取退出码 3.6. 如何进行非阻塞等待 3.7. 补充  4. 进程的程序替换 4.0. 为什么要有进程的程序替换 4.1. 进程的程序替换是什么 4.2. 进程的程序替换的演示 4.3. 利用fork创建子进程来执行进程替换 4.4. exec系列函数的运用和理解 4.4.1. execl函数 4.4.2. execv函数 4.4.3. execlp函数 4.4.4. execvp函数 4.4.5. execle函数 4.4.6. execvpe函数 4.4.7. execve函数 4.4.8. 补充如何通过makefile一次形成两个可执行程序 5. 实现简陋版本的shell重新认识shell运行原理 1. 进程创建 我们这里谈论的进程创建其实就是fork创建子进程函数原型如下 # man 2 fork man 2号手册#include unistd.h pid_t fork(void); 至于fork返回值在这里就不细说了我们今天的主题不是它 请你描述一下fork创建子进程OS都做了什么 我们知道fork()的功能是create a child process 既然是创建一个子进程那么本质上就是OS里面多了一个进程 而我们知道一个进程需要有与进程强相关的内核数据结构包括PCB、地址空间、页表还要有进程的代码和数据 多了一个进程就多了与进程强相关的内核数据结构同时会将会将代码和数据Load到物理内存中并通过页表构建好地址空间和物理内存的映射关系并将对应的PCB放入运行队列里等待CPU或者调度器进行调度该进程 一般情况下这些内核数据结构是由OS维护的 代码和数据一般是从磁盘中来的也就是加载到内存的C/C的可执行程序 1.1. 内核数据结构的处理 我们需要知道进程是需要具有独立性的 内核数据结构的处理 因此当创建子进程时OS必须分配新的内存块和内核数据结构给子进程(定义的过程)然后会将父进程的部分内核数据结构的内容拷贝给子进程(赋值的过程) 1.2. 代码的处理 可是代码和数据是如何处理的呢 同理由于进程具有独立性因此理论上子进程也必须要有自己的代码和数据 可是一般而言fork创建子进程没有加载(将可执行程序Load到物理内存)的过程也就是说此时子进程没有自己的代码和数据所以子进程只能使用 父进程的代码和数据 可是现在就出现问题了如果父子进程中的其中一个对数据甚至代码发生了修改那么此时如何处理呢 对于代码而言一般情况下都是不可以被写的只能进行读因此再一般情况下父子进程共享代码没有问题 注意fork之后父子进程代码共享指的是所有代码都是共享的 然而对于数据而言在很多情况下都有可能发生数据的修改因此父子进程的数据在很多情况下都会各自私有一份 1.3. 数据的处理 因此对于数据而言一般有两种处理方案 方案一fork创建子进程的时候直接对数据进行拷贝处理让父子进程各自私有一份 但是我们在进程下也讨论过一些问题 其一 当fork创建这个子进程之后一定会被立刻运行吗 其二 即便你准备要立刻运行这个子进程那么你这个子进程一定会访问所有与子进程相关的数据吗 其三 即便你这个子进程要访问所有的数据那么一定都是以写的方式访问所有数据吗 也就是说这种无脑的拷贝数据带来的问题就是拷贝的数据空间子进程可能根本就不会访问甚至即便去访问了也可能只是读取而并非写入  例如 void Test1(void) {const char* str1 cowsay hello;const char* str2 cowsay hello;printf(str1: %p\n,str1);printf(str2: %p\n,str2); } 可以看到str1和str2是指向的同一个字符串也就是说编译器编译程序的时候尚且知道节省空间本质上还是OS对于只读数据只会维护一份罢了 因此结论就是fork创建子进程根本不需要将不会被访问的或者只读的数据拷贝一份此时父子进程共享这些数据即可 那么什么数据需要被拷贝呢 很简单将来会被父进程或者子进程写入的数据需要拷贝一份父子进程各自私有这些数据 可是一般而言即便是OS也很难预测到哪些数据会被写入即便当OS预测到了某些数据会被写入那么当提前拷贝了这些空间你会立刻使用吗答案是不一定立刻使用既然存在着不会立刻使用的可能那么OS提前拷贝这些数据不就是在浪费空间吗 因此OS选择了写实拷贝技术来进行将父子进程的数据进行分离 方案二写实拷贝(copy on write) 写实拷贝技术很好理解通常情况下父子进程的代码是共享的父子进程当不对数据进行写入时数据也是共享的而当父子进程任意一方对数据或者代码进行修改就会进行拷贝让父子进程各自私有一份在这里有一份图帮助理解 使用写实拷贝的原因 其一OS无法在代码执行前预知那些空间被写入 其二当用的时候在给你分配内存是高效使用内存的一种表现 写实拷贝本质就是一种延迟申请技术来提高整机的使用率那么也就提高了整机的效率 因为有写实拷贝技术的存在所以父子进程得以彻底分离完成了进程独立性的技术保证 1.4. fork常规用法 一个父进程希望复制自己使父子进程同时执行不同的代码段。例如父进程等待客户端请求生成子进程来处理请求。 一个进程要执行一个不同的程序。例如子进程从fork返回后调用exec系列函数 1.5.fork失败的原因 系统中有太多的进程 实际用户的进程数超过了限制 创建进程的成本是非常高的(体现在时间 空间上)例如内核数据结构的创建与维护是需要消耗一定的时间和空间的并且在Linux下普通用户创建进程的数量是有限制的 1.6. 扩展 我们的代码汇编之后会有很多行代码而且每行代码Load到内存之后都有对应的地址 因为进程随时可能被中断(可能并没有执行完)下次被调度的时候还必须从之前的位置继续运行(不是最开始的位置)那么就要求CPU必须随时记录下当前进程执行的位置所以CPU内有对应的寄存器数据用来记录当前进程的执行位置而这个寄存器称之为EIP也称之为PC(point code)指针即程序计数器这个PC指针记录的永远都是当前正在执行代码的下一行代码的地址 其实硬件很无脑的例如物理内存就是用来存储和读取数据的而不会对读写做什么限制同样的CPU很无脑的CPU根本不知道自己在干什么它就做几件事取指令、分析指令、执行指令 分析指令和执行指令的前提就是CPU必须认识这些指令一般的CPU都有指令集 寄存器再CPU内部只有一套寄存器但是寄存器内的数据是可以有多份的而寄存器内的数据称之为进程的上下文数据 既然是数据fork创建的时候要不要给子进程呢 答案是当然要 由于父进程执行fork的时候EIP的值就是fork之后的代码而fork创建子进程这份数据(EIP的数据)也要以写实拷贝的方式给子进程因此子进程的EIP的值也是fork之后的代码故fork之后子进程会从fork之后的代码执行而不是从所有代码的起始位置执行 2. 进程终止 进程终止本质上就是OS内少了一个进程那么当然要释放进程申请的相关内核数据结构和对应的数据和代码本质上就是在释放系统资源(这个资源主要是内存) 2.1. 进程退出场景 进程退出的场景如下 1、代码跑完进程正常终止结果正确 2、代码跑完进程正常终止结果不正确 3、代码没有跑完进程异常终止 注意前两种情况为正常退出只不过结果是否正确罢了而第三种情况为异常终止说明进程还没有执行完代码就终止了 2.2. 什么是退出码  在以前学习C/C的过程中为什么我们的main的返回值大多数情况下都是0呢这个0代表着什么意思呢 答案是首先这里的main的返回值并不是只能是0其次这里的这个整数代表着这个进程的退出码 补充根据C和C的标准规定main 函数必须具有返回类型为 int。返回值的作用是向操作系统指示进程的执行状态通常 0 表示进程正常终止且结果正确非零值表示进程正常终止且结果错误虽然根据标准main 函数必须有返回值但是在某些特定的情况下编译器可能会允许省略返回值如果省略那么默认返回值就是0但是虽然有些编译器或者IDE可以省略返回值但是为了提高代码的可读性和可移植性建议在 main 函数中显式指定返回值类型并返回适当的值。 而在Linux下在命令行上用 echo $? 可以输出最近一个进程正常终止时的退出码注意异常终止的进程的退出码无意义 例如 代码跑完进程正常终止结果正确 int main() {printf(haha\n);return 0; } 代码跑完进程正常终止结果不正确 int main() {printf(haha\n);return 10; } 可以发现在main中的return的这个值代表着进程的退出码 2.3. 退出码如何表现的  我们也知道退出码本质上是C语言的一种处理错误的机制 当一个进程正常终止的时候如果进程的结果正确我们往往不关心为什么正确但是如果结果错误我们往往更想知道进程结果错误的原因是什么这时候我们就需要用 非0 这种多组值来表示不同的错误原因。 然而退出码有多组值因此有时候用户并不能知道具体的某个退出码代表的是什么退出信息因此我们可以根据一些函数得到退出码所表示的退出信息例如 #man 3 strerror #include string.h char *strerror(int errnum); 上面的这个函数的功能将一个退出码转换成一个退出信息的字符串并返回给上层用户例如(在这里只展示一部分的退出码和退出信息) 可以看到 C语言为我们提过的退出码有134个包括0也可以看到当退出码等于0时代表着进程正常终止并且结果正确当退出码为非0时代表着进程正常终止并且结果错误 当然如果你不想使用C为我们提供的这一套退出码你也可以在自己的代码呢中定义一套错误码供自己使用 2.4. 退出码的意义 OK你告诉我main的return的值代表着这个进程的退出码可是这个退出码有什么意义呢 首先要强调一点退出码只对正常终止的进程才具有意义对非正常终止的进程毫无意义 这个退出码的意义返回给这个进程的父进程父进程用这个退出码来判定子进程的执行结果0代表着子进程正常终止且结果正确非0值代表着子进程正常终止且结果不正确当结果不正确时不同的值可以表示不同的错误原因 如何理解退出码只对正常终止的进程才有意义 生活中的场景 李四是大学二年级的一名学生准备考线代了 情况一李四考完了之后没有挂科然后李四给他老爹说老爹我考试过了这时候老爹难道会问你为什么过了其实一般情况下老爹是不会这样问的既然你都过了正常的达到了预期结果那么我就不会关心你为什么会达到这种结果类比到进程当一个进程正常终止的时候且结果正确那么退出码就是0就代表着success 情况二李四考完了挂科了然后李四给他老爹说老爹我考试挂科了线代才考了20分那么正常情况下老爹当然会问你为什么才考这么低也就是相当于老爹想知道我没有达到预期结果的原因类比到进程当子进程正常终止的时候并且结果错误的话那么关心它的进程也就是它的父进程自然需要得到它的退出码通过退出码表示子进程的错误原因 情况三李四考试过程中作弊被逮住了然后李四给他老爹说老爹我考试作弊被逮住了此时老爹难道还会问你你考了多少分考的情况怎么样答案是不会因为你是作弊被逮住了就算你此时考了满分、考了0分都没有任何意义因为这样的结果是不被认可的是没有意义的结果类比到进程当一个进程异常终止的时候此时退出码对于这个进程而言是没有任何意义的 因此退出码只对正常终止的进程才具有意义 2.5. 退出码的演示 2.5.1. 代码跑完进程正常终止结果正确 当一个进程代码跑完正常终止且结果正确那么退出码就为0 2.5.2. 代码跑完进程正常终止结果不正确 可以发现当退出码为1时错误信息的确是 Operation not permitted因此以后我们自己写退出码时也不要胡写尽量符合标准 2.5.3. 代码异常终止 void Test1(void) {printf(haha\n);printf(haha\n);printf(haha\n);int i 10 / 0;printf(hehe\n);printf(hehe\n);printf(hehe\n); } 此时对于这个进程的父进程来说既然这个子进程异常终止了进程跑都没跑完(进程崩溃了)那么退出码也就没有意义了。 2.6. 如何正常的终止一个进程 进程正常终止 1、main里面的return 2、调用  C库函数  exit 3、调用 系统调用接口 _exit 2.6.1. main里面的return int get_sum(int top) {int sum 0;for(int i 0; i top; i){sum i;}return sum; }int main() {printf(hahaha\n);printf(hahaha\n);printf(hahaha\n);int top 10;int sum get_sum(top);printf(sum %d\n,sum);return 123;printf(hehehe\n);printf(hehehe\n);printf(hehehe\n);return 0; } 综上的结果那么我们有非main()的return只是单纯的代表着函数返回值罢了不会终止进程而main的return就代表了终止进程而后面的数字就代表了进程的退出码 2.6.2. exit // man 3 exit // exit - cause normal process termination#include stdlib.hvoid exit(int status); 可以看到exit在man的二号手册。实际上它是一个C库函数它的功能就是导致一个进程正常终止exit函数的参数status就是进程的退出码例如 int main() {printf(haha\n);printf(haha\n);exit(111);printf(hehe\n);printf(hehe\n);return 0; }可以看到exit也可以使当前进程终止并且其参数就是进程的退出码 那么它与return有什么区别呢 void print(void) {printf(cowsay hello\n);printf(cowsay hello\n);exit(222);printf(hello world\n);printf(hello world\n); }int main() {printf(haha\n);printf(haha\n);print();exit(111);printf(hehe\n);printf(hehe\n);return 0; }可以看到一个进程只要遇到了exit就会终止进程也就是说exit和return的区别return只有在main才会终止进程而exit在任意调用的地方都代表着终止进程  2.6.3. _exit man 2 _exit //terminate the calling process#include unistd.h void _exit(int status); void _Exit(int status); // The function _Exit() is equivalent to _exit() 首先_exit函数是一个系统调用接口并且它的功能也是任意地方调用_exit都代表着终止进程实际上exit的底层调用的就是_exit这个系统调用接口而关于_exit的演示就不在这里重复演示了我想说的是exit和_exit的一个区别用这个差异说明一个问题 2.6.4. _exit和exit的一个区别 看代码 int main() {printf(you can see me?\n);sleep(3);exit(222);return 0; } 我们看到的现象先看到打印消息然后进程休眠三秒进程退出退出码为222 如果更改为下面的代码  int main() {printf(you can see me?);sleep(3);exit(222);return 0; } 看到的现象先休息三秒再打印消息而导致这种错觉的原因是因为printf函数会先将数据写入输出缓冲内当调用该exit这个库函数接口时会先去将输出缓冲区的内容刷新到显示器内在终止进程实际上还是先打印消息在休息三秒只不过打印的数据暂时存放在输出缓冲区内罢了  而我们看看如果是_exit它会怎么做呢 int main() {printf(you can see me?);sleep(3);_exit(222);return 0; }现象是休息了三秒钟但却没有打印消息 为什么 注意_exit是一个系统调用接口exit是一个库函数调用而我们看到的现象是当调用库函数exit时进程终止前会刷新缓冲区而调用系统调用接口进程终止了都没有将缓冲区的数据刷新到显示器内那么说明这个缓冲区一定不是OS维护的如果这个缓冲区是OS维护的那么两者都应该刷新那么既然不是OS维护的那么这个缓冲区究竟是谁维护的呢  要知道这个exit是谁为我们提供的是C标准难道说这个缓冲区是C语言为我们提供的 答案是是的下面这个图更能说明问题 在这里就想说明一个问题C语言是为我们提供了缓冲区的例如printf就会先向C缓冲区写入数据exit函数在终止进程之前会做一些进程的收尾工作例如刷新缓冲区(C语言的缓冲区)关闭流等。而_exit强制终止进程不会进行进程的收尾工作例如不会刷新缓冲区、不会关闭打开的文件描述符、不会执行终止处理程序等。 因此如果在_exit调用之前有缓冲区未被刷新的输出例如使用 printf 或 fprintf 输出的内容这些内容将不会被写入到相应的输出设备或文件中。这可能导致输出结果不完整或不一致。 此外未关闭的文件描述符可能会造成资源泄露。文件描述符是操作系统用于访问文件、套接字和其他 I/O 设备的标识符。正常情况下进程退出时会自动关闭打开的文件描述符释放相关资源。但是使用 _exit强制终止进程时这些文件描述符可能会保持打开状态从而导致资源泄露。 综上使用_exit应该谨慎确保在调用之前完成必要的收尾工作例如手动刷新缓冲区(fflush)和关闭打开的文件描述符以避免数据丢失和资源泄露等问题。因此一般情况下推荐使用C库函数exit来正常终止进程因为它会执行标准的进程收尾工作。 3. 进程等待 进程等待是什么 进程等待指的是一个进程等待另一个进程的退出一般情况下当fork创建子进程后需要父进程等待子进程退出得到子进程的退出信息以及回收子进程的资源 进程等待为什么 获取子进程退出的信息可以保证时序问题一般需要让子进程先退出父进程后退出(回收子进程)。进程退出的时候会先进入Z状态(kill -9 也无能为力)需要通过父进程wait()/waitpid(),释放该子进程占用的资源避免了内存泄露。 3.1. wait等待子进程 // man 2 wait // wait for process to change state #include sys/types.h #include sys/wait.hpid_t wait(int *status);// return val: # on success,returns the process ID of the terminated child; # on error, -1 is returned; 首先我们演示一下僵尸进程思路如下fork创建子进程子进程运行三次然后终止父进程一直死循环运行当子进程退出时就会成为僵尸状态需要父进程回收 void Test2(void) {pid_t id fork();if(id 0){perror(fork failure);exit(-1);}else if(id 0){// child processint cnt 3;while(cnt--){printf(i am a child process ,cnt: %d,PID: %d,PPID: %d\n,cnt,getpid(),getppid());sleep(1);}exit(0);}else{// parent processwhile(1){printf(i am a parent process,PID: %d,PPID: %d\n,getpid(),getppid());sleep(1);}} } // 同样用一个shell命令行监控脚本 while :; do ps ajx | head -1 ps ajx | grep my_test | grep -v grep; sleep 1; echo ; done 现象如下 那么用wait如何处理僵尸进程呢即wait如何等待子进程处理逻辑如下子进程执行三秒进程退出成为僵尸进程父进程先休眠五秒然后开始等待 void Test3(void) {pid_t id fork();if(id 0){perror(fork failure);exit(-1);}else if(id 0){// child processint cnt 3;while(cnt--){printf(i am a child process ,cnt: %d,PID: %d,PPID: %d\n,cnt,getpid(),getppid());sleep(1);}exit(0);}else{// parent processsleep(5);printf(i am a parent process,PID: %d, PPID: %d\n,getpid(),getppid());printf(wait begin!\n);pid_t ret wait(NULL);printf(ret: %d\n,ret);printf(wait success!\n);} } 可以看到当父进程休眠完五秒的时候此时子进程是一个僵尸状态调用wait开始回收子进程并且可以看到wait的返回值就是被回收的子进程的PID实际上当父进程调用wait时是一种阻塞等待也就是说此时父进程的状态会成为阻塞状态本质上是OS将这个进程的PCB添加到了一个等待队列中等待子进程状态改变(实质上就是等待子进程死掉)当子进程终止成为僵尸状态那么OS会重新将父进程的PCB从等待队列链接到运行队列中让CPU或者调度器调度这个父进程使其执行wait系统调用接口回收子进程的资源 3.2. waitpid等待子进程 #include sys/types.h #include sys/wait.hpid_t waitpid(pid_t pid, int *status, int options); 参数的介绍 第一个参数 pidpid代表着你要等待哪一个进程的ID号。 当pid -1时代表着等待任意一个子进程 当pid 0时那么就等待ID号与pid相等的哪个进程 第三个参数 optionsoptions代表着等待进程的方式 options 0时代表着阻塞等待 options WNOHANG时代表着非阻塞等待 那么用waitpid如何处理僵尸进程呢即waitpid如何等待子进程处理逻辑如下子进程执行三秒进程退出成为僵尸进程父进程等待子进程退出 void Test1(void) {pid_t id fork();if(id 0){perror(fork error!);exit(-1);}else if(id 0){// child processint cnt 3;while(cnt--){printf(i am a child process,cnt: %d,PID: %d,PPID: %d\n,cnt,getpid(),getppid());sleep(1);}exit(0);}else{// parent processprintf(i am a parent process,PID: %d,PPID: %d\n,getpid(),getppid());printf(begin wait child process:\n);pid_t ret waitpid(id,NULL,0);printf(wait success,ret: %d\n,ret);} } 可以看到 父进程调用waitpid时当第三个参数也就是option为0时此时就代表着阻塞等待本质上是OS将父进程添加到等待队列里即状态为阻塞状态等待子进程退出当子进程退出后OS又将父进程链接到运行队列里让CPU或者调度器调度父进程回收子进程资源 3.3. 如何获取子进程的exit code 我们知道进程等待要做两件事情1、回收子进程资源2、得到子进程的退出码 可是我们如何通过进程等待这两个函数获得子进程的退出结果呢 答案是int* status这个status是一个输出型参数本质上当父进程等待子进程退出的时候这个status的值是由OS为我们填充的 void Test2(void) {pid_t id fork();if(id 0){perror(fork error!);exit(-1);}else if(id 0){// child processint cnt 3;while(cnt--){printf(i am a child process,cnt: %d,PID: %d,PPID: %d\n,cnt,getpid(),getppid());sleep(1);}exit(123);}else{// parent processprintf(i am a parent process,PID: %d,PPID: %d\n,getpid(),getppid());printf(begin wait child process:\n);int status 0;pid_t ret waitpid(id,status,0);printf(wait success,ret: %d\n,ret);printf(child process exit code: %d\n,status);} } 不对啊我子进程的退出码是123啊 原因是因为这个status并不是按照整数来整体使用的而是按照比特位的方式 而我们只使用status的低16位如下图所示 当进程正常终止的时候上面这16个位的次低八位表示子进程的退出码 代码如下 void Test3(void) {pid_t id fork();if(id 0){perror(fork error!);exit(-1);}else if(id 0){// child processint cnt 3;while(cnt--){printf(i am a child process,cnt: %d,PID: %d,PPID: %d\n,cnt,getpid(),getppid());sleep(1);}exit(123);}else{// parent processprintf(i am a parent process,PID: %d,PPID: %d\n,getpid(),getppid());printf(begin wait child process:\n);int status 0;pid_t ret waitpid(id,status,0);printf(wait success,ret: %d\n,ret);// 次低八位代表子进程的退出码printf(child process exit code: %d\n,(status 8) 0xFF);} } 验证成功次低八位代表着子进程的退出码父进程可以根据子进程的退出码判断子进程正常终止时结果是否正确  3.4. 如何获取子进程的退出信号 在说进程终止的时候我们探讨过进程异常退出或者崩溃本质是操作系统杀掉了你这个进程 操作系统如何杀掉你这个进程的 本质是通过发送信号的方式 换句话说当一个进程异常终止的时候本质是这个进程收到了某个信号导致其终止 那么如何获取子进程的退出信号 status的低7位就是子进程的退出信号如下 void Test4(void) {pid_t id fork();if(id 0){perror(fork error!);exit(-1);}else if(id 0){// child processint cnt 3;while(cnt--){printf(i am a child process,cnt: %d,PID: %d,PPID: %d\n,cnt,getpid(),getppid());sleep(1);}exit(123);}else{// parent processprintf(i am a parent process,PID: %d,PPID: %d\n,getpid(),getppid());printf(begin wait child process:\n);int status 0;pid_t ret waitpid(id,status,0);printf(wait success,ret: %d\n,ret);// 次低八位代表子进程的退出码printf(child process exit code: %d\n,(status 8) 0xFF);// 低7位代表这个子进程的退出信号printf(child process exit signal: %d\n,status 0x7F);} }一般情况下当子进程正常终止的时候那么退出信号为0也就是说当收到的信号为0时代表进程是正常终止的 那么进程异常终止是怎样的呢演示如下 此时子进程多了一个除0错误我们看看会发生什么 void Test5(void) {pid_t id fork();if(id 0){perror(fork error!);exit(-1);}else if(id 0){// child processint cnt 3;while(cnt--){printf(i am a child process,cnt: %d,PID: %d,PPID: %d\n,cnt,getpid(),getppid());sleep(1);}int i 10 / 0;exit(123);}else{// parent processprintf(i am a parent process,PID: %d,PPID: %d\n,getpid(),getppid());printf(begin wait child process:\n);int status 0;pid_t ret waitpid(id,status,0);printf(wait success,ret: %d\n,ret);// 次低八位代表子进程的退出码printf(child process exit code: %d\n,(status 8) 0xFF);// 低7位代表这个子进程的退出信号printf(child process exit signal: %d\n,status 0x7F);} }可以看到此时子进程就收到了一个信号我们看看这个8号信号究竟是什么  SIGFPE  ---  Floating Point Exception即浮点数异常错误 当发生浮点运算错误时操作系统会发送 SIGFPE 信号给相应的进程终止这个进程 当进程异常终止时status的低7位代表着进程的退出信号并且可以发现此时进程的退出码为0因为当进程异常终止时进程的退出码毫无意义 这也反面的验证了进程的退出码只有当进程正常终止时才有意义当进程异常终止时退出码毫无意义 有时候进程异常不光光是内部代码逻辑有问题也可能是外力直接杀掉例如 3.5. 用操作系统提供的宏帮助我们获取退出码 在3.3.中我们是以位运算的方案获取正常终止的进程的退出码而OS为了更简化这个过程为我们提供了两个宏分别是WIFEXITED、WEXITSTATUS WIFEXITED(status)查看进程是否正常终止如果进程正常终止那么这个宏的结果就是真(方便记忆 W --- wait IF --- if  EXITED --- 退出  ) WEXITSTATUS(status)若WIFEXITED为真(即进程正常终止)那么提取子进程退出码退出码就是WEXITSTATUS(status)(方便记忆W --- wait EXIT --- 退出  STATUS --- 状态) 具体如下 void Test1(void) {pid_t id fork();if(id 0){perror(fork failed);exit(-1);}else if(id 0){// child processint cnt 10;while(cnt--){printf(i am a child process,cnt: %d,PID: %d,PID: %d\n,cnt,getpid(),getppid());sleep(1);}}else {// parent processprintf(i am a parent process,PID: %d,PPID: %d\n,getpid(),getppid());printf(begin wait\n);int status 0;pid_t ret waitpid(-1,status,0);if(WIFEXITED(status)){// 进程正常终止,获取进程退出码printf(wait child process:(PID: %d),child process normal exit,exit code: %d\n,ret,WEXITSTATUS(status));}else{// WIFEXITED(status) 0 --- 进程异常终止printf(child process get a signal\n);// 如果还想获取子进程的退出信号,那么就用位运算的方案获取printf(child process abnormal exit signal: %d\n,status 0x7F);}} }具体细节就不再做过解释了与上面的方式没有要大的差别只不过多了用宏判断子进程如何终止以及用宏获取子进程正常终止的退出码 3.6. 如何进行非阻塞等待 上面进程等待的处理方案都是一种阻塞等待即父进程等待子进程的时候父进程是处于阻塞状态的本质是在等待过程中OS将父进程的PCB链接到了某个等待队列中罢了当子进程退出时OS又将这个父进程的PCB链接到运行队列里进而让CPU或者调度器调度父进程回收子进程的资源 然而我们也可以以非阻塞等待的方式进行等待子进程退出回收子进程资源本质上是在等待过程中父进程并不是处于阻塞状态而是处于运行状态同时我们可以借助waitpid的返回值达到采用轮询的方式实现非阻塞等待即在等待期间不断地用waitpid的返回值检查子进程的状态以便在子进程状态改变后立即得到通知。 需要注意的是在进行非阻塞等待时父进程可能会因为过于频繁地调用等待函数而产生性能问题。因此父进程一般需要针对具体的应用场景合理选择合适的等待方式以兼顾程序的性能和功能需求。 总之在父进程进行非阻塞等待时父进程会继续执行并处于运行状态以便在子进程的状态改变时立即得到通知并做出响应。 那么waitpid如何实现非阻塞等待呢 答案是option这个参数 以前说过option为0时代表默认行为即阻塞等待 而当 option为 WNOHANG时代表非阻塞等待(便于理解 W --- wait NO --- 不 HANG --- 夯住了) 夯住了在系统层面不就是这个进程没有被CPU调度(要么这个进程的PCB在阻塞队列中要么是等待被调度)而WNOHANG就是代表这个进程不要被夯住 grep -ER WNOHANG /usr/include/ 可以看到WNOHANG就是一个#define定义的一个符号常量其值就是1而这里之所以不直接用1是为了代码的可读性(没有夯住没有阻塞)即避免产生了魔鬼数字 waitpid的返回值 pid_t waitpid(pid_t pid, int *status, int options);// 当options 被设置为 WNOHANG 那么: # return val:# val 0 : 子进程未退出,返回值为0# val 0 : 等待成功,返回0# val 0 : 等待失败,返回0 说了这么多还是要以代码举例的 void Test2(void) {pid_t id fork();if(id 0){perror(fork failed);exit(-1);}else if(id 0){// child processint cnt 10;while(cnt--){printf(i am a child process,cnt: %d,PID: %d,PID: %d\n,cnt,getpid(),getppid());sleep(1);}}else {// parent processwhile(1){int status 0;pid_t ret waitpid(-1,status,WNOHANG);if(ret 0){// wait failedprintf(wait failed\n);exit(-1);}else if(ret 0){// waitpid successprintf(i am a parent process,PID: %d,PPID: %d\n,getpid(),getppid());if(WIFEXITED(status)){printf(child process normal exit\n);printf(wait child(PID: %d) success,exit code: %d\n,ret,WEXITSTATUS(status));}else{printf(child process abnormal exit\n);printf(get a exit signal: %d\n,status 0x7F);}break;}else{// ret 0// 子进程未退出,waitpid返回0,父进程继续等待子进程退出(轮询方案)printf(child process no exit,parent process can do own things!\n);}sleep(1); // 让父进程每一秒检查一次,这就是基于轮询方案的非阻塞等待}} } 实现逻辑很简单通过waitpid的三种返回值设计出不同的逻辑 当返回值小于0时意味着父进程等待失败了那么设置好退出码终止父进程 当返回值大于0时意味着子进程终止需要父进程回收子进程的资源同时可以判断子进程是否正常终止以便于获取子进程的退出码或者退出信号 当返回值等于0时那么说明子进程未退出那么需要父进程重新进行调用waitpid等待子进程退出也就是在这个逻辑下父进程可以在等待子进程的同时可以处理自己的业务 而上面的这种方案我们称之为基于非阻塞调用的轮询检测方案 为了更好体现非阻塞等待我们可以让父进程等待的同时处理一些自己的业务如下 void func1(void) {printf(service logic 1\n); }void func2(void) {printf(service logic 2\n); }typedef void(*p_func)();std::vectorp_func v_func;void Load() {v_func.push_back(func1);v_func.push_back(func2);v_func.push_back([](){printf(service logic 3\n);}); }void Test3(void) {pid_t id fork();if(id 0){perror(fork failed);exit(-1);}else if(id 0){// child processint cnt 5;while(cnt--){printf(i am a child process,cnt: %d,PID: %d,PID: %d\n,cnt,getpid(),getppid());sleep(1);}exit(111);}else {// parent processwhile(1){int status 0;pid_t ret waitpid(id,status,WNOHANG);if(ret 0){printf(wait failed\n);exit(-1);}else if(ret 0){// waitpid successprintf(i am a parent process,PID: %d,PPID: %d\n,getpid(),getppid());if(WIFEXITED(status)){printf(child process normal exit\n);printf(wait child(PID: %d) success,exit code: %d\n,ret,WEXITSTATUS(status));}else{printf(child process abnormal exit\n);printf(get a exit signal: %d\n,status 0x7F);}break;}else{// ret 0 // 子进程未退出的同时让父进程执行自己的业务逻辑if(v_func.empty()) Load();printf(child process no exit,parent process do own thisng: \n);for(auto iter : v_func){iter();}}sleep(1); // 让父进程每一秒检查一次,这就是基于轮询方案的非阻塞等待}} } 现象如下 上面主要演示的就是父进程通过Load加载自己在等待过程中所需处理的各种业务逻辑 总结来说非阻塞等待是一种在等待事件完成时不会阻塞当前执行流程的机制通过返回值或者信号来通知事件的发生并允许进程继续执行其他任务。在waitpid函数中可以通过设置选项参数WNOHANG来实现非阻塞等待的效果。 下面这段伪代码也可以帮助我们更好的理解waitpid函数 pid_t waitpid(id, status, flag) {// 下面是内核中waitpid的实现,属于操作系统的// 检测子进程退出状态,查看子进程的PCB中子进程的运行信息if(子进程退出){// 回收子进程的各种资源(子进程状态由Z-X)// OS根据子进程PCB中的exit_code 和 exit_signal填充status,如下status | (child-exit_code 8);status | (child-exit_signa);return child_pid;}else if(子进程没退出){if(flag 0){// 阻塞等待挂起父进程; // 本质就是将父进程的PCB链接到了等待队列中// 因此进程阻塞的本质:是进程阻塞在系统函数的内部// 而当父进程被重新唤醒的时候,不是重新调用waitpid,而是从上次被挂起的地方// 的后面继续运行}else if(flag WNOHANG) return 0; // 不阻塞父进程// 因此非阻塞等待的本质: // 就是当waitpid检测到子进程没退出时,waitpid直接返回了// 因此父进程可以执行waitpid后面的业务逻辑// 同时可以进入下一次检测(轮询方案的检测机制)return 0;}else{// 出错了等其他原因return -1;} } 3.7. 补充  父进程通过 wait/waitpid 可以拿到子进程的退出结果和退出信号为什么要用wait/waitpid函数呢直接使用全局变量不行吗 答案不可以因为进程具有独立性当子进程修改全局变量时这个全局数据就要发生写实拷贝父进程无法拿到子进程设置的这个全局变量况且还有信号如何设置 那么既然进程具有独立性进程退出码不也是子进程的数据吗父进程有凭什么能拿到呢wait/waitpid究竟干了什么呢 当子进程终止时那么子进程就会成为僵尸状态当成为僵尸状态时子进程至少要保留该进程的PCB信息task_struct里面保留了任何进程退出时的退出结果信息父进程调用wait/waitpid中的status这个数据来源本质是子进程的task_struct 结构中的字段 那么也就是说进程的PCB里面有两个字段如下 struct task_struct {int exit_code;int exit_signal;// ... }; 因此wait/waitpid可以得到退出码和退出信号的本质就是将子进程PCB中的这两个字段通过位操作设置到你传入的这个status里并返回给上层用户 4. 进程的程序替换 4.0. 为什么要有进程的程序替换 目前我们创建的子进程子进程和父进程执行的是同一份代码只不过两个进程执行的代码逻辑块可能不一样罢了 但如果我想让子进程执行全新的代码呢即为什么要有进程的程序替换 让子进程执行一个 全新的程序 当然还有其他原因例如 程序更新当一个程序需要更新版本时旧的程序可以通过新的版本来替换这样就不需要停止并重新启动进程从而减少了系统的维护和操作负担。在新版本启动之前进程仍能执行旧程序的任务。 资源节约通过程序替换可以让进程在不创建新进程的情况下更改自身的执行内容从而节约了系统资源。 软件开发有时开发人员需要在一个进程中动态替换执行程序为调试和测试提供更方便的方式。 4.1. 进程的程序替换是什么 我们将一个进程的内核数据结构不变仅仅替换当前进程代码和数据的技术叫做进程的程序替换那么也就是说进程的程序替换并不会重新创建一个子进程而是更改当前进程的代码和数据和页表的映射关系罢了 如何理解将磁盘上的可执行程序放入到内存中(从一个硬件到另一个硬件)这个过程不就是加载吗而我们知道编译有编译器链接有链接器而加载也有加载器所谓的exec系列的接口的本质就是如何加载可执行程序的函数即加载器的底层就是这些exec系列的函数 补充 Linux的加载器loader在底层使用了exec系列的函数。exec系列函数是用于加载和执行新程序的函数族在Linux中被广泛使用。exec系列的函数可以将一个新的可执行程序加载到当前进程的地址空间并执行实现新程序的替换。当Linux加载器启动时它会使用exec函数加载并执行指定的可执行文件从而创建一个新的进程并运行程序。这个过程中加载器会解析可执行文件的格式将程序的代码、数据和其他资源加载到适当的内存区域并设置正确的执行环境最终调用exec函数执行程序。因此可以说Linux的加载器底层使用了exec系列的函数来完成加载和执行可执行文件的任务。 4.2. 进程的程序替换的演示 void Test1(void) {printf(hahahahaha\n);printf(----------\n);printf(hahahahaha\n); } 上面的代码没啥好说的如果我想在打印分割后执行新的程序该如何实现呢我们需要借助exec系列的函数帮助我们实现进程的程序替换 # man 3 execl --- 3号手册// path: 所要执行的可执行程序的路径 // arg: 预计要如何执行这个程序(命令行上如何写的,这个指针数组就填什么,并以NULL结尾) int execl(const char *path, const char *arg, ...); 代码如下 void Test1(void) {printf(hahahahaha\n);printf(----------\n);execl(/usr/bin/ls,ls,-l,-a,-i,NULL);printf(hahahahaha\n); }可以看到调用execl成功之后会将当前进程所有的代码和数据进行替换包括已经执行的和没有执行的同时会去执行新的可执行程序 那么也就是说execl调用成功之后原有代码会被替换掉(原有的后续代码全部都不会被执行)因此execl根本不需要进行返回值来判断只要调用execl函数如果还执行了execl后续代码那么代表着execl一定出错了此时终止进程即可。 4.3. 利用fork创建子进程来执行进程替换 在我们以前的认知中fork创建子进程会以代码共享、数据写实拷贝的方式创建子进程然而在进程的程序替换的场景中由于进程的程序替换会更改当前进程的代码和数据因此在这种场景下父子进程的代码和数据都会以写实拷贝的方式处理各自私有一份(代码 数据) 为什么创建子进程呢 答案是为了不影响父进程我们想让父进程聚焦在读取数据、解析数据、指派子进程执行代码的功能如果不创建子进程那么进程的程序替换就会把父进程自身的代码和数据给替换了如果创建了子进程那么我们可以做到被替换的是子进程的代码和数据父进程通过写实拷贝保证自己数据和代码的独立性 处理代码如下 void Test1(void) {pid_t id fork();if(0 id){// child processprintf(i am a child process,PID: %d,PPID: %d\n,getpid(),getppid());printf(begin execute execl:\n);execl(/usr/bin/ls,ls,-a,-l,-i,NULL);exit(-1); // 如果子进程走到这里,就说明execl调用失败了,进程终止}else{// parent processprintf(i am a parent process,PID: %d,PPID: %d\n,getpid(),getppid());printf(begin wait:\n);int status 0;waitpid(id,status,0);if(WIFEXITED(status)){printf(child process normal exit,exit code: %d\n,WEXITSTATUS(status));}else{printf(child process abnormal exit,exit single: %d\n,status 0x7F);}} } 通过execl可以使得父子进程执行不同的可执行程序父进程等待子进程退出子进程执行全新的可执行程序 进程的程序替换的意义让进程执行新的可执行程序只要进程的程序替换成功就不会执行后续代码意味着exec*()的函数成功的时候。不需要返回值检测只要exec*返回了就一定是因为exec系列的函数调用失败了。 4.4. exec系列函数的运用和理解 // man 3 exec*() // 库函数#include unistd.h extern char **environ; 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[]);# man 2 execve // 系统接口 int execve(const char *filename, char *const argv[], char *const envp[]); 这些函数如果调用成功则加载新的程序并开始执行 , 不再返回 如果调用出错则返回 -1 所以exec系列函数只有出错的返回值而没有成功的返回值 接下来有一些关于这些函数名的理解 l(list) : 表示参数采用列表的形式 v(vector) : 参数用指针数组 p(path) :自动在环境变量 PATH中搜索目标程序 e(env) : 表示自己维护环境变量 4.4.1. execl函数 # man 3 execl#include unistd.h/* * path: 代表你要执行哪一个可执行文件,此时要明确可执行文件的路径(相对/绝对) * arg: 代表你要如何执行这个可执行文件,在命令行上如何执行,这里就如何将参数一个一个传递进去 * 注意: 最后要以NULL结尾作为参数传递的结束 * ... --- 可变参数列表 */ int execl(const char *path, const char *arg, ...); execl的示例代码  void Test1(void) {pid_t id fork();if(id 0){execl(/usr/bin/ls,ls,-i,-l,-a,NULL);exit(1);}else{int status 0;waitpid(id,status,0);if(WIFEXITED(status)){printf(child process normal exit,exit code: %d\n,WEXITSTATUS(status));}else{printf(child process abnormal exit,get a exit signal\n);}} } 4.4.2. execv函数 # man 3 execv/* * path: 代表着你要执行哪一个可执行文件? 包括这个文件的路径文件名 * argv: 是一个指针数组,内容代表着你要如何执行这个可执行文件 * 即把命令行上怎么执行的一个一个参数写进这个指针数组里面 * 注意最后这个指针数组要以NULL结尾,标志着参数传递的结束 */ int execv(const char *path, char *const argv[]); execv的示例代码   void Test2(void) {pid_t id fork();if(id 0){//int execv(const char *path, char *const argv[]);char* const argv[] {ls,-i,-l,-a,NULL};execv(/usr/bin/ls,argv);exit(1);}else{int status 0;waitpid(id,status,0);if(WIFEXITED(status)){printf(child process normal exit,exit code: %d\n,WEXITSTATUS(status));}else{printf(child process abnormal exit,get a exit signal\n);}} } 4.4.3. execlp函数 # man 3 execlp/* * file你要执行谁,通过文件名,OS自动在环境变量PATH中搜索可执行文件 * arg: 你要如何执行这个可执行程序(要执行的可执行程序在命令行上怎么执行 * 这里的参数就一个一个的传递进去); * 注意: 最后以NULL结尾 * ...: 代表着可变参数列表 */ int execlp(const char *file, const char *arg, ...); execlp的示例代码 void Test3(void) {pid_t id fork();if(id 0){//int execlp(const char *file, const char *arg, ...);execlp(ls,ls,-a,-l,-i,NULL);exit(1);}else{int status 0;waitpid(id,status,0);if(WIFEXITED(status)){printf(child process normal exit,exit code: %d\n,WEXITSTATUS(status));}else{printf(child process abnormal exit,get a exit signal\n);}} }4.4.4. execvp函数 那这个函数就更简单了因为函数名带p那么就只需要传递文件名操作系统自动在环境变量PATH中搜索该可执行文件并且第二个参数是一个指针数组我们只需要把命令行上怎么执行的一个一个参数写进一个指针数组里面最后要以NULL结尾。 # man 3 execvp/* * file: 代表着可执行程序的文件名,OS自动在环境变量PATH中搜索 * argv: 是一个指针数组,内容为命令行参数 * 注意: 最后要以NULL结尾,标志着参数传递的结束 */ int execvp(const char *file, char *const argv[]); execvp的示例代码 void Test4(void) {pid_t id fork();if(id 0){// int execvp(const char *file, char *const argv[]);char* const argv[] {ls,-a,-l,-i,NULL};execvp(ls,argv);exit(1);}else{int status 0;waitpid(id,status,0);if(WIFEXITED(status)){printf(child process normal exit,exit code: %d\n,WEXITSTATUS(status));}else{printf(child process abnormal exit,get a exit signal\n);}} }4.4.5. execle函数 # man 3 execle/* * path: 你要执行谁(要执行的可执行程序的全路径,即所在路径/文件名 * arg: 你要如何执行(命令行上如何执行的,你就一个一个将参数传递进来) * 注意: 最后要以NULL结尾,标志着参数传递的结束 * ...: 可变参数列表 * envp: 可以自定义环境变量 */ int execle(const char *path, const char *arg, ..., char * const envp[]); execle的示例代码 // proc.c 源文件#include stdio.hint main() {extern char** environ;for(int i 0; environ[i]; i){printf(environ[%d]: %s\n,i,environ[i]);}return 0; }如果单独执行由proc.c源文件生成的可执行程序的话运行这个可执行程序proc那么打印的环境变量就是如下 // test.c 源文件void Test5(void) {pid_t id fork();if(id 0){//int execle(const char *path, const char *arg, ..., char *const envp[]);char* const envp[] {MYENV1 hahahahahahaha\n,MYENV2 hahahahahahaha\n,MYENV3 hahahahahahaha\n,MYENV4 hahahahahahaha\n,NULL};execle(./proc,proc,NULL,envp);exit(1);}else{int status 0;waitpid(id,status,0);if(WIFEXITED(status)){printf(child process normal exit,exit code: %d\n,WEXITSTATUS(status));}else{printf(child process abnormal exit,get a exit signal\n);}} }我们可以通过进程的程序替换将我们自己定义的env传给proc那么proc可执行程序成为进程后会打印我们自己定义的环境变量如下 我们以前一直说子进程会继承父进程的环境变量那么现在我们就知道了父进程可以通过execle这种函数将环境变量导给子进程因此看到的现象是子进程继承了父进程的环境变量故环境变量具有全局属性 4.4.6. execvpe函数 # man 3 execvpeint execvpe(const char *file, char *const argv[], char *const envp[]); 这个函数跟execle几乎一样的只不过第一个参数是通过环境变量PATH进行搜索第二个参数是一个指针数组内容为命令行参数最后要以NULL结尾第三个参数可以自己定义环境变量在这里就不做演示了 4.4.7. execve函数 # man 2 execve/* * filename: 你要执行谁(文件的路径 文件名) * argv: 你要如何执行(argv是一个指针数组,内容为命令行参数) * 注意: 最后要以NULL结尾,标志着参数传递的结束 * envp: 可以自己定义环境变量 */int execve(const char *filename, char *const argv[], char *const envp[]); 与前面不同的是前面的函数都是库函数(严格意义讲这些是C语言对execve这个系统调用的封装)而execve是一个系统调用换句话说前面的库函数都是对这个系统调用execve的封装之所以提供了这些封装其目的是为了满足了不同的调用场景 void Test6(void) {pid_t id fork();if(id 0){//int execve(const char *filename, char *const argv[], char *const envp[]);char* const envp[] {MYENV1 hahahahahahaha\n,MYENV2 hahahahahahaha\n,MYENV3 hahahahahahaha\n,MYENV4 hahahahahahaha\n,NULL};char* const argv[] {ls,-a,-l,-i,NULL};execve(/usr/bin/ls,argv,envp);exit(1);}else{int status 0;waitpid(id,status,0);if(WIFEXITED(status)){printf(child process normal exit,exit code: %d\n,WEXITSTATUS(status));}else{printf(child process abnormal exit,get a exit signal\n);}} }下面这个图说明了exec系列函数的一些关系 4.4.8. 补充如何通过makefile一次形成两个可执行程序 # makefile文件:my_test:test.cgcc -o $ $^ -stdgnu99 proc:proc.cgcc -o $ $^ -stdgnu99 .PHONY:clean clean:rm -f my_test proc 调换一下makefile中生成可执行程序文件的顺序可以吗 proc:proc.cgcc -o $ $^ -stdgnu99 my_test:test.cgcc -o $ $^ -stdgnu99 .PHONY:clean clean:rm -f my_test proc 通过上面的测试我们知道makefile默认只会形成在依赖关系中形成第一个依赖文件。 那么如何才能通过makefile一次性生成两个可执行程序呢 因此我们可以这样操作 .PHONY:all all:proc my_testproc:proc.cgcc -o $ $^ -stdgnu99 my_test:test.cgcc -o $ $^ -stdgnu99 .PHONY:clean clean:rm -f my_test proc 因为all是一个伪目标所以它总是被执行的。又因为all有依赖关系所以make的时候它需要生成all那么必须先要生成proc和my_test但是又因为没有依赖方法所以最后all不会生成结果如下 此时我们就可以通过makefile一次性生成两个(多个)可执行文件 5. 实现简陋版本的shell重新认识shell运行原理 shell执行的命令通常有两种第三方命令和内建命令 1. 第三方提供的对应的在磁盘中有具体二进制文件的可执行文件由子进程执行 2. shell内部自己实现的方式由父进程自己来进行执行。因为有些命令就是要影响shell这个进程本身的例如cdexport enum MAX {CMD_MAX 128,ARGV_MAX 64 };void Test7(void) {char command[CMD_MAX] {0};while(1){// step 1: 打印提示符printf([Xq#MY-LOCAL-LINUX]$ );// step 2: 获取命令行字符串command[0] 0; // 以O(1)的方式清空字符串fgets(command,CMD_MAX,stdin); // 注意我们上面的这个字符串,是有一个\n的// 例如ls\n\0; 因此我们要将这个\n 置为 \0command[strlen(command) - 1] 0; // 将\n -- \0// step 3: 解析字符串// 用于存放命令行参数的指针数组char* argv[ARGV_MAX] {NULL};// 定义分隔符const char* delim ; // 一般情况下,分隔符都是空格int i 0;argv[i] strtok(command,delim);while((argv[i] strtok(NULL,delim)));// 打印当前解析的字符串// 检测是否正确for(int i 0; argv[i]; i){printf(argv[%d]: %s\n,i,argv[i]);}// step 5:处理内建命令// 例如cd 以内建命令的方式进行运行,不创建子进程,让父进程shell自己运行// 内建命令---不创建子进程让父进程自己运行if(0 strcmp(cd,argv[0])){chdir(argv[1]);continue;}// step 4: 处理第三方命令// 子进程通过进程的程序替换执行新的可执行程序// 父进程充当shell,回收子进程,如果子进程结果不正确,应该得到子进程的退出结果pid_t id fork();if(id 0){// 处理第三方命令execvp(argv[0],argv); //execvp 太合适不过了! ! !exit(1); // 如果走到这,那么execvp一定出错了}else{// 父进程等待子进程退出即可int status 0;waitpid(-1,status,0);if(WIFEXITED(status)){if(WEXITSTATUS(status) ! 0){printf(child process exit code: %d\n,WEXITSTATUS(status));}}else{printf(child process get a exit signal\n);}}} }这就是关于shell的简陋实现
http://www.zqtcl.cn/news/488621/

相关文章:

  • 做h5的网站页面设计软文素材网站
  • 黄冈网站推广软件费用是多少手机网站弹出层插件有哪些
  • wordpress文章链接怎么改怎么优化关键词排名优化
  • 专业做包包的网站好产品网站做营销推广
  • 网站刚建好怎么做能让百度收录湖北黄石网站建设
  • 网站建设拾金手指下拉二一wordpress 插件破解
  • 天津做网站外包公司有哪些美橙互联网站
  • 石家庄网站建设蓝点办公室装修工程
  • 申请网站空间就是申请域名建设机械网站咨询
  • 做美食网站有哪些网站怎么做自响应
  • 衡水网站建设维护宝安官网网站建设比较好的
  • 网站建设的审批重庆建设工程信息网30系统
  • 泉州软件开发培训机构怎么做网站内部链接的优化
  • 网站定位是什么中国it外包公司排名
  • 洛阳微信平台网站建设网站成功案例分析
  • 网站建设在淘宝怎么分类深圳软件开发招聘信息
  • .net如何做网站个人网站的制作
  • 网站优化排名推广站长统计官方网站
  • 长沙wap网站建设wordpress 用户 函数
  • 淮安做网站的公司有哪些公司目前上海有几个区
  • 怎么做自动跳转网站建站之星 discuz
  • 网站建设开发合同范本页面设计有哪几种风格
  • 重庆做网站重庆做网站做公司网站建设价格
  • 住房建设部官方网站公示公告国内卖到国外的电商平台
  • 安徽省建设厅网站巅川建设有限公司宁波城乡建设网站
  • 做财务还是网站运营wordpress主题 微博
  • 为什么要用CGI做网站网站建设 自学 电子版 pdf下载
  • 建设网站的规则营销型网站建设jm3q
  • 深圳建网站价格防水堵漏公司做网站效果怎样
  • 网站建设东莞老铁博客外国炫酷网站网址