5父子进程关系及GDB多进程调试
使用GDB调试时,GDB默认只能跟踪一个进程,可以再fork调用前,通过指令设置GDB调试工具跟踪父进程或跟踪子进程,默认跟踪父进程
下面的这些命令都是gdb命令 不是命令行命令
设置调试子进程 set follow-fork-mode child 这种情况下且set detach-on-fork为on则会停在子进程的断点处 父进程会一直运行直到跑完
默认调试父进程 set follow-fork-mode parent如果在这种情况下在父进程内打断点 然后运行父进程会停在断点处 子进程不会管 只会继续运行然后跑完
在gdb内可查看当前gdb追踪的是什么进程 show follow-fork-mode
设置调试模式 set detach-on-fork [on|off] 默认为on,表示调试当前进程的时候其他进程继续运行,如果为off调试当前进程时,其他进程被GDB挂起(会停在fork那句话 等待我们切换调试)。
gdb版本为8.x set detach-on-fork off会出问题 7.x 9.x可以用这个功能
查看当前程序的所有进程 会打印出进程在gdb中的编号:info inferiors
切换当前调试进程:inferior id(id为进程在gdb中的编号)
使进程脱离GDB调试:detach inferiors id (执行后id号进程不再受gdb控制就一直向下执行)
移除某进程:remove inferiors id
gcc fork_test.c -o hello -g
gdb hello
父子进程之间的关系
区别:
1 fork()函数的返回值不同 父进程中>0:返回的子进程id 子进程=0
2 pcb中的一些数据 比如父进程和子进程的pid ppid都是不同的
共同点:
某些状态下:子进程刚被创造出来,还没有执行任何写数据的操作时,用户区的数据文件描述符表两个进程是相同的 (读时共享)。只要发生写操作 那么发生写时拷贝,两个进程的这个被写对象就不同了。
6 exec函数族
exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容。
换句话说就是在调用进程内部执行一个可执行文件。
exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段数据段堆栈等(所有用户区内容)都被新的可执行文件的内容取代,只留下进程ID等一些表面上的信息(一些内核区内容)仍保持原样。(exec不会生成新进程 而是取代现有的这个进程)只有调用失败了,它们才会返回-1,从原程序的调用点接着往下执行。
exec后从可执行文件的main开始执行
可以这么做,fork子进程再在子进程中调用exec取代子进程 这样不会影响父进程。
//标准C库#include <unistd.h>extern char **environ;int execl(const char *path, const char *arg, .../* (char *) NULL */);int execlp(const char *file, const char *arg, .../* (char *) NULL */);int execle(const char *path, const char *arg, .../*, (char *) NULL, 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[]);//unix库#include <unistd.h>int execve(const char *filename, char *const argv[],char *const envp[]);
l(list)参数地址列表,以空指针结尾
v(vector)存有各参数地址的指针数组地址
p(path)按PATH环境变量指定的目录搜索可执行文件
e(environment)存有环境变量字符串地址的指针数组的地址
/*#include <unistd.h>extern char **environ;int execl(const char *path, const char *arg, .../ (char *) NULL /);参数:path 可执行程序的路径(绝对相对都行) 推荐用绝对路径arg 其实是可变参数 可以写很多 后面的。。。就是arg1 arg2 ... argn字符串指针 指向一系列字符串参数列表./a.out hello fuck 12在执行可执行文件时 后面跟着的参数会被main arg获得这个arg其实就是exec可执行文件的main的传入参数列表第一个arg一般没有作用 一般写的是执行的可执行文件的名称从第二个参数往后才是可执行文件main的真正参数列表这个参数列表一定要以NULL为结尾,这样才知道参数列表读入完毕返回值只有在执行可执行文件失败才会返回-1 并设置errno*/#include <unistd.h>#include <stdio.h>int main(){//创建一个子进程 在子进程中执行exec函数pid_t pid = fork();if (pid > 0){//父进程printf("father pid:%d", getpid());}else{//子进程printf("child pid:%d", getpid()); //放上面 放在execl下面不会执行execl("/home/wen/lesson16/main", "main", NULL); //参数列表 第一个参数一般写可执行文件的名称 参数列表以null结尾//可以执行shell文件命令 ps aux查看进程信息// execl("/bin/ps", "ps", "aux", NULL);// execl("/bin/ps", "ps", "a", "u", "x", NULL);//这两种都对}for (int i = 0; i < 3; i++) //这个for只有父进程才会执行 因为//exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段数据段堆栈等(所有用户区内容)都被新的可执行文件的内容取代// ,只留下进程ID等一些表面上的信息(一些内核区内容)仍保持原样 exec不会生成新进程 而是取代现有的这个进程{printf("i:%d pid:%d", i, getpid());}return 0;}
execlp
/*#include <unistd.h>extern char **environ;int execlp(const char *file, const char *arg, .../ (char *) NULL /);会到环境变量中的路径中查找指定的同名文件 找到了就执行 找不到返回-1参数:file 可执行文件的文件名 a.out ps 可以不用写绝对路径 直接写文件名arg(同execl)这个arg其实就是exec可执行文件的main的传入参数列表第一个arg一般没有作用 一般写的是执行的可执行文件的名称从第二个参数往后才是可执行文件main的真正参数列表这个参数列表一定要以NULL为结尾,这样才知道参数列表读入完毕返回值只有在执行可执行文件失败才会返回-1 并设置errno*/#include <unistd.h>#include <stdio.h>extern char **environ;int main(){//创建一个子进程 在子进程中执行exec函数pid_t pid = fork();if (pid > 0){//父进程printf("father pid:%d", getpid());}else{//子进程printf("child pid:%d", getpid()); //放上面 放在execl下面不会执行//在环境变量路径中需要存在main这个文件// execlp("main", "main", NULL); //参数列表 第一个参数一般写可执行文件的名称 参数列表以null结尾//可以执行shell文件命令 ps aux查看进程信息// ps是在 /bin目录下的 环境变量中是包含这个/bin目录的 命令:env 其中有个PATH 为环境变量中包含的路径execlp("ps", "ps", "aux", NULL);}for (int i = 0; i < 3; i++) //这个for只有父进程才会执行 因为//exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段数据段堆栈等(所有用户区内容)都被新的可执行文件的内容取代// ,只留下进程ID等一些表面上的信息(一些内核区内容)仍保持原样 exec不会生成新进程 而是取代现有的这个进程{printf("i:%d pid:%d", i, getpid());}return 0;}
int execv(const char *path, char *const argv[]);//argv 参数字符串数组不再需要像上面 那样一个一个参数写到函数里char * myagrv[] = {"ps","aux",NUll};execv("/bin/ps", myagrv);int execve(const char *filename, char *const argv[],char *const envp[]);//第一个参数可执行文件名(不是路径)//第二个参数 可执行文件main的参数字符串数组 argv数组中,第一个元素一定需要把路径写对//第三个参数envp数组参数不是用来查找可执行程序的,而是为可执行程序运行期间增加新的环境变量//下面这个例子是通过//args中第一个参数一定要把路径写对char *args[] = {"./a.out", "a.out", (char *)0};//(char *)0 就是NULL//env_args为我们执行的execlp新增了环境变量,这样excelp中可以找到hello这个可执行程序char *env_args[] = {"PATH=/home/wen/lesson15",(char*)0};execve("execlp", args, env_args);
7进程控制
进程退出
#include <stdlib.h> //标准Cvoid exit(int status);//多做了 调用退出处理函数 刷新IO缓冲区关闭文件描述符这两件事#include <unistd.h>// linux系统函数void _exit(int status);
![]() ![]() |
|---|
/*#include <stdlib.h> //标准Cvoid exit(int status);//多做了 调用退出处理函数 刷新IO缓冲区关闭文件描述符这两件事status 进程退出时的状态信息。父进程在回收子进程资源时可以获取到这个状态信息#include <unistd.h>// linux系统函数void _exit(int status);*/#include <stdlib.h> //标准C#include <stdio.h>#include <unistd.h>int main(){printf("hello\n");printf("world");exit(0); //在return0上调用 return不会被执行//终端上打印结果//hello//worldwen@wenc: 因为world没换行 所以后面紧跟着终端信息_exit(0);//终端打印结果//hello//wen@wenc: 并没有打印world//因为hello带上了\n 会自动做刷新缓冲区(先将hello放入缓冲区 再接收\n将缓冲区刷新 打印出hello)//但是world printf("world")只是将world放入缓冲区中 但是并没有刷新缓冲区//exit是会做刷IO缓冲区的 但是_exit不会做 所以用exit world能被打印出来 _exit world没有打印出来return 0; // main返回0给shell 相当于我们的程序进程调用了exit(0)}
孤儿进程
父进程结束 但子进程还在运行(未运行结束),这样的子进程称为孤儿进程。
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init(PID为1的进程),而init进程会循环地wait()孤儿进程。当一个孤儿进程结束生命周期的时候,init进程就会去处理它的善后工作(回收资源)。因此孤儿进程并不会有什么危害。
wen@wenc:….. ./learn_orphan
im father pid 14042 ppid 2512 (就是执行的这个终端)
….//父进程先死亡
//当父进程先子进程死亡,会由后台切换到执行父进程的那个终端前台(因为这个终端是父进程的父进程),所以会先打印出终端信息wen@wenc: ,然后因为子进程还在执行所以会将子进程用了printf也会将信息输出到终端前台im child pid:1403 ppid:1,会什么子进程的信息也会输出到当前的前台内因为fork后父子进程的文件描述符表是相同的(没写之前是共享的),文件描述符表的前三个是固定的标准输出、标准输入、标准错误,因为共享文件描述符所以其标准输出的文件fd是相同的都是指向当前终端前台的。
wen@wenc:im child pid:1403 ppid:1(孤儿进程被init进程接管)
僵尸进程
每个进程结束之后,都会释放自己地址空间中的用户区数据,内核区的PCB没办法自己释放需要父进程区释放。
子进程终止时,父进程没有做回收操作,子进程残留资源(PCB)在内核中,变成僵尸进程。
僵尸进程无法用kill -9杀死。 需要将僵尸进程的父进程杀死,这样僵尸进程就变为孤儿进程被init线程托管处理(回收PCB)。
这样会导致一个问题。如果父进程不调用wait()或waitpid()(这两个函数用于等待回收资源),那么在内核中保留的PCB信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果有大量的僵尸进程,将会出现因为没有可用的进程号而导致系统不能产生新的进程,这就是僵尸进程的危害应当避免。
8wait函数
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等等。但是仍然为其保留一定的信息,这些信息主要指PCB的信息(进程号、退出状态、运行时间等)。
父进程可以通过调用wait或waitpid()得到它的退出状态同时彻底清除掉这个进程。
wait()和waitpid()函数的功能一样,区别在于,wait()函数会阻塞(只用当子进程死亡wait才不阻塞),waitpid()可以设置不阻塞,waitpid()还可以指定等待哪个子进程结束。
注意 一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
/*#include <sys/types.h>#include <sys/wait.h>pid_t wait(int *wstatus);子进程终止 被信号停止 被信号挂起 wait将不阻塞子进程终止这种情况wait将会释放与子进程相关的资源(pcb)如果父进程不调用wait或waitpid子进程将会变为僵尸进程如果子进程已经发生了状态改变,这个系统调用将会立即返回。否则将会阻塞(父进程挂起(阻塞))直到这个父进程的一个子进程状态改变或有信号终止这个系统调用参数: wstatus进程退出的状态信息 由函数传出WIFEXITED(wstatus) 非0 进程正常退出WEXITSTATUS(wstatus)如果上宏WIFEXITED为真 则为进程的退出状态(exit的参数)WIFSIGNALED(wstatus)非0 进程异常终止WTERMSIG(wstatus)如果上宏WIFSIGNALED为真 获取使进程终止的信号编号WIFSTOPPED(wstatus)非0 进程处于暂停状态(挂起)WSTOPSIG(wstatus)如果上宏WIFSTOPPED为真 获取使进程暂停的信号的编号WIFCONTINUED(wstatus)非0 进程暂停后已经继续运行返回值:成功返回被回收的子进程的id失败返回-1(所有子进程都结束立刻返回 没有子进程立刻返回 或调用函数失败 这三种情况都会返回-1)pid_t waitpid(pid_t pid, int *wstatus, int options);都是用于等待子进程状态的改变(并非只用于子进程死亡回收资源)*/#include <sys/types.h>#include <sys/wait.h>#include <unistd.h>#include <stdio.h>int main(){// fork()// fork()// 执行2次 注意并不是产生两个子进程哦// 第一次fork后的子进程在的二次fork还好产生孙子进程算上本身共有4个进程pid_t pid;//一个父进程 创建5个子进程(5个子进程互相为兄弟)for (int i = 0; i < 5; i++){pid = fork();if (pid == 0) //刚产生子进程分支 在子进程种这个fork的返回值是0{break; //不让任意一个子进程分支再执行fork 假如执行fork这样会产生孙子进程而不是子进程}//只有父进程的第一次fork的返回值不是0是子进程的pid 才能继续循环}if (pid > 0) //父进程的fork返回值才大于0{while (1){//父进程printf("im father pid = %d\n", getpid());int status;pid_t child = wait(&status);printf("recyle one child pid:%d", child);if (child == -1)break;//一个子进程被回收if (WIFEXITED(status)) //为真 则是正常终止{int subret = WEXITSTATUS(status); // 如果上宏WIFEXITED(status)为真 则为进程的退出状态(exit的参数)//subret 即子进程main return的东西 或子进程exit(参数)的参数}else if (WIFSIGNALED(status)) //为真 则是异常终止{int termsig = WTERMSIG(status); //如果上宏WIFSIGNALED为真 获取使子进程终止的信号编号}sleep(1); //打印慢一些}}else if (pid == 0) //子进程的第一次fork返回值是0{//子进程while (1){printf("im child pid = %d\n", getpid());sleep(1); //打印慢一些}}// 实验 我们通过命令kill -9 子进程 来终止子进程使wait不阻塞 父进程调用完wait后能看到recyle语句被打印// 当所有子进程被kill recyle语句打印的pid为-1 表示所有子进程都已经结束了return 0;}
waitpid
/*#include <sys/types.h>#include <sys/wait.h>pid_t waitpid(pid_t pid, int *wstatus, int options);回收指定pid的子进程 可以设置此函数是否阻塞调用wait(&status) 等用于调用waitpid(-1,&status,0)函数默认阻塞 直到子进程被终止参数:ps ajx 可以查看进程组ID输入的pid>0,等待对应PID的子进程结束 回收资源(PCB)pid = 0,等待任意与父进程 pgid(进程组ID)相同的子进程结束 回收资源(PCB)pid = -1,等待任意子进程结束 回收资源(PCB)pid < -1,等待任意pgid(进程组ID)与输入的pid的绝对值(!!!) 相同的子进程结束 回收资源(PCB)即回收pgid==|输入的pid| 的子进程资源父进程的ID一般就为组ID,子进程的进程组一般和父进程是同一组wstatus进程退出的状态信息 由函数传出WIFEXITED(wstatus) 非0 进程正常退出WEXITSTATUS(wstatus)如果上宏WIFEXITED为真 则为进程的退出状态(exit的参数)WIFSIGNALED(wstatus)非0 进程异常终止WTERMSIG(wstatus)如果上宏WIFSIGNALED为真 获取使进程终止的信号编号WIFSTOPPED(wstatus)非0 进程处于暂停状态(挂起)WSTOPSIG(wstatus)如果上宏WIFSTOPPED为真 获取使进程暂停的信号的编号WIFCONTINUED(wstatus)非0 进程暂停后已经继续运行option 用于阻塞或非阻塞0 阻塞WNOHANG 一个宏值,非阻塞 无子进程结束立即返回WUNTRACEDWCONTINUED返回值>0 返回的是结束子进程的id=0 非阻塞情况下(WNOHANG) 无子进程结束返回0-1 出错 或 没有子进程了*/#include <sys/types.h>#include <sys/wait.h>#include <unistd.h>#include <stdio.h>int main(){// fork()// fork()// 执行2次 注意并不是产生两个子进程哦// 第一次fork后的子进程在的二次fork还好产生孙子进程算上本身共有4个进程pid_t pid;//一个父进程 创建5个子进程(5个子进程互相为兄弟)for (int i = 0; i < 5; i++){pid = fork();if (pid == 0) //刚产生子进程分支 在子进程种这个fork的返回值是0{break; //不让任意一个子进程分支再执行fork 假如执行fork这样会产生孙子进程而不是子进程}//只有父进程的第一次fork的返回值不是0是子进程的pid 才能继续循环}if (pid > 0) //父进程的fork返回值才大于0{while (1){//父进程printf("im father pid = %d\n", getpid());sleep(1); //打印慢一些int status;// pid_t child = wait(&status);// pid_t child = waitpid(-1, &status, 0); //阻塞 回收所有子进程pid_t child = waitpid(-1, &status, WNOHANG); //阻塞 回收所有子进程printf("recyle one child pid:%d", child);if (child == -1) //所有子进程都结束都已经被回收完了 或出错返回-1break;else if (child == 0){//非阻塞 无子进程结束返回0continue;}else if (child > 0){//返回大于0 表示有一个子进程结束 并被waitpid回收了//一个子进程被回收if (WIFEXITED(status)) //为真 则是正常终止{int subret = WEXITSTATUS(status); // 如果上宏WIFEXITED(status)为真 则为进程的退出状态(exit的参数)//subret 即子进程main return的东西 或子进程exit(参数)的参数}else if (WIFSIGNALED(status)) //为真 则是异常终止{int termsig = WTERMSIG(status); //如果上宏WIFSIGNALED为真 获取使子进程终止的信号编号}}}}else if (pid == 0) //子进程的第一次fork返回值是0{//子进程while (1){printf("im child pid = %d\n", getpid());sleep(1); //打印慢一些}}// 实验 我们通过命令kill -9 子进程 来终止子进程使wait不阻塞 父进程调用完wait后能看到recyle语句被打印// 当所有子进程被kill recyle语句打印的pid为-1 表示所有子进程都已经结束了return 0;}


