进程创建在代码中进程创建用的是fork函数调用fork函数后操作系统会为子进程分配内存块和进程控制块PCB并将父进程PCB的部分内容拷贝至子进程。接着将子进程添加到系统进程列表中也就是系统管理进程的链表中最后fork函数返回调度器调度子进程。我们知道为了提高内存空间的利用率减少创建子进程的时间在父进程刚创建子进程时数据和代码是二者共用的其中一方需要修改某一部分数据时才会触发写时拷贝。而触发写时拷贝的关键就是页表中的权限。在创建子进程前父进程的数据段是可以修改的也就是页表中标记了该部分有写的权限但当子进程创建后就会被改为只读的。若父子进程中有一方尝试通过虚拟地址修改数据段的内容由于内存管理单元MMU通过页表发现这个地址是只读的经过一系列的信息传递最终操作系统会收到这个修改只读区域的错误但操作系统经过检测发现这个虚拟地址属于该进程的数据段而数据段是可以修改的于是触发写实拷贝。如下进程退出进程会在两种情况下退出一是代码运行完毕二是异常终止。对于前者main函数的返回值就是进程退出时的退出码通常表明了程序的运行情况返回0表示运行结果正确返回非0则表示结果错误。返回值会写到进程的task_struct内部。使用命令 echo $ 即可查看上一个结束的进程的退出码也就是main函数的返回值。如果是异常终止的情况进程的退出码就没有意义了。使用函数strerror即可查看退出码对应的含义。如下#includestdio.h #includestring.h int main(){ for(int i0;i200;i){ printf(error[%d] : %s\n,i,strerror(i)); } return 0; }也可以调用exit函数让进程主动退出在代码的任何地方调用exit函数都会让进程退出。此时的退出码是传入exit的参数。如exit(3)就返回3。还有另一个类似的函数是_exit函数二者的区别是 _exit 属于系统调用函数exit则属于C语言的库函数exit内部就是通过调用_exit终止进程的进程使用exit退出时会对各个缓冲区进行刷新如果是用_exit退出则会直接终止进程。进程等待子进程退出后其父进程需要接收子进程的退出信息并回收资源否则子进程就会进入僵尸状态用命令都杀不掉毕竟已经死了。那么具体如何操作呢答案是调用函数wait或waitpid进行等待当然不只是等子进程退出子进程退出后这两个函数会接收子进程的退出信息并回收资源彻底终结子进程。下面是进程等待的两个函数wait和waitpid的定义其中_Nullabel是一个类型修饰符type qualifier用于表示指针可以为空。这两个函数都会将子进程的退出信息传到参数wstatus指向的空间这种用于输出的参数被称为输出型参数这些子进程的退出信息就是操作系统从子进程的task_struct中拷贝过来的。我们先看比较简单的wait函数。wait的原理就是让父进程阻塞在wait调用处一直等待直到子进程退出。等待成功会返回回收的子进程的pid否则一般返回-1wait函数不会返回0。等待结束后从参数wstatus传出的并不是退出码而是类似位图的信息模式。我们知道int类型有32位的空间在wait传出的这32位的信息中左边16位无意义不用管右边的16位如下如果该进程是正常退出的左起8位就是退出码右边8位无意义。如果是异常退出的左起8位无意义右边8位中最左边的一位是core dump标志暂时不用管剩下的7位是终止信号。如果不想通过位运算提取退出码可以使用库中定义的宏如下WIFEXITED(status)用于查看进程是否是正常退出。若为正常终止子进程返回的状态则为真反之则为假。WEXITSTATUS(status)用于查看进程的退出码。若WIFEXITED非零也就是正常退出则提取子进程退出码。终止信号以及其它信号相关内容会在之后讲解信号相关内容时一起讲解。接着我们来看waitpid函数它的功能更全面返回值和参数wstatus的用法和wait函数一样。比较特殊的是waitpid可以返回0这个后面讲解。waitpid函数可以指定等待哪个或哪些子进程参数pid为-1时waitpid会和wait一样等待所有子进程大于0时则是等待对应pid的进程等于0的情况可以先不管它的功能是等待同一个进程组的所有子进程。参数options是用来选择等待方式的。前面提到调用wait时如果子进程未运行完毕父进程会阻塞在wait调用处这种等待方式叫做阻塞式等待waitpid函数如果选项参数options为0也是采用这种等待方式。如果要采用非阻塞等待的方式则可以使用选项WNOHANG它是宏定义的 1。具体来说传入该选项后waitpid函数只会检查一次子进程是否结束如果结束了就正常接收退出信息回收资源如果没结束也不会一直等待子进程结束而是直接返回0这就是非阻塞等待。waitpid返回值中的0就是这个用法。不过为了确保能够回收子进程一般是把传入WNOHANG选项的waitpid函数写进循环中不断检查子进程是否运行结束的同时还可以执行其它代码这时可以使用函数指针数组每次循环调用不同的函数如下。这种父进程没有被阻塞但是不停地检查的等待方式叫做非阻塞轮询。要注意虽然WNOHANG的值是1但不要直接用1代替一是这样降低代码可读性二是在某些系统下有出错的风险。#includestdio.h #includeunistd.h #includesys/wait.h void Download(){ printf(下载已开始\n); sleep(1); printf(下载已结束\n); } void function1(){ printf(功能一已启用\n); sleep(1); printf(功能一已结束\n); } void function2(){ printf(功能二已启用\n); sleep(1); printf(功能二已结束\n); } void function3(){ printf(功能三已启用\n); sleep(1); printf(功能三已结束\n); } void(*func[5])(void); int main(){ func[0]Download; func[1]function1; func[2]function2; func[3]function3; int pidfork(); if(pid0){//子进程 sleep(10); } else{//父进程 int wstatus,i0; while(!waitpid(pid,wstatus,WNOHANG)){ if(i5 func[i]){ func[i](); } else{ printf(准备就绪正在等待子进程结束...\n); sleep(2); } i; } if(WIFEXITED(wstatus)) printf(子进程运行完成返回值为%d\n,WEXITSTATUS(wstatus)); else printf(子进程运行失败\n); } return 0; }如果忘记函数指针数组的相关知识可以看我之前写的文章C语言指针的初步了解与深入解析-CSDN博客进程程序替换使用execl函数可以实现让代码使用命令。path是指令对应文件的路径剩下的就是命令的内容命令怎么写我们就怎么传按空格划分命令再传入然后以NULL结尾即可。int execl(const char *path, const char *arg, ...);比如我们要使用命令 ls -a -l 就用下面的方式传入execl(usr/bin/ls,ls,-l,-a,NULL);多个选项组合在一起的形式不用另外拆分比如 ls -al 就分成 ls 和 -al 传入即可。调用execl函数后该进程剩下的代码不再被执行而是被替换为命令ls这就是程序替换。在程序替换的过程中没有创建新的进程只是用新的进程的代码和数据覆盖式地替换当前进程的代码和数据。程序替换成功后原始代码中处于execl函数后面的部分就不存在了。所以替换成功时execl函数不会返回任何值只有替换失败才会返回-1。 如果既想要执行命令又想保留程序剩下的部分可以用fork创建子进程让子进程去调用execl函数。子进程的代码段原本是和父进程共用的当进行程序替换时代码段也会触发写时拷贝。像这样以exec开头的可以使用命令的函数一共有7个#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 execve(const char *path, char *const argv[], char *const envp[]); int execvpe(const char *file, char *const argv[], char *const envp[]);事实上,只有execve是真正的系统调用,其它六个个函数最终都调用execve。这些函数看起来很容易记混但其实是有命名规则在的​l (list)​: 参数以列表​的形式传递就是像上面那样逐个直接写入函数参数中。​v (vector)​: 参数以数组​的形式传递即传入一个以NULL结尾的字符串指针数组。​p (path)​: 带有该后缀的函数会通过环境变量 PATH​ 来查找可执行文件。不需要完整路径提供文件名即可。​e (env)​: 带有该后缀的函数可以传入环境变量​数组并用传入的环境变量数组覆盖原有的环境变量数组。如果想传入现有的环境变量可以将头文件unistd.h里面的字符二级指针char** environ传入。exec函数的本质其实就是找到可执行文件然后用传入的命令调用。因此除了使用指令外exec函数还可以调用各种程序。方法是一样的命令怎么写参数就怎么传。这样就可以实现在C语言代码中调用Java程序、C程序甚至是脚本。这些exec函数其实就是加载器底层调用的接口。加载器也是操作系统的一部分主要功能是将程序从硬盘或其他存储设备加载到内存中并为其运行做好一切准备。关于加载器感兴趣的可以自行了解。本文到此结束感谢观看