1.3.2 getpid函数
获取当前进程ID
pid_t getpid(void);
1.3.3 getppid函数
获取当前进程的父进程ID
pid_t getppid(void);
区分一个函数是“系统函数”还是“库函数”依据:
② 是否访问内核数据结构
② 是否访问外部硬件资源 二者有任一 → 系统函数;二者均无 → 库函数
1.3.4 getuid函数
获取当前进程实际用户ID uid_t getuid(void); 获取当前进程有效用户ID uid_t geteuid(void);
1.3.5 getgid函数
获取当前进程使用用户组ID gid_t getgid(void); 获取当前进程有效用户组ID gid_t getegid(void);
1.3.6 进程共享
父子进程之间在fork后。有哪些相同,那些相异之处呢? 刚fork之后: 父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式... 父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集 似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗? 当然不是!父子进程间遵循读时共享写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。 练习:编写程序测试,父子进程是否共享全局变。 【fork_shared.c】 重点注意!躲避父子进程共享全局变量的知识误区! 【重点】:父子进程共享:1. 文件描述符(打开文件的结构体) 2. mmap建立的映射区 (进程间通信详解)
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int var =100; int main() { pid_t pid; pid = fork(); if(pid == -1){ perror("fork error"); exit(1); }else if(pid>0) { var = 288; printf("parent ,var = %d\n",var); printf("i'am parent pid = %d,getppid =%d\n",getpid(),getppid()); } else if(pid==0) { var = 200; printf("i'am child pid =%d,ppid =%d\n",getpid(),getppid()); printf("child,var = %d\n",var); } printf("-------finish----------\n"); return 0; }
特别的,fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。
1.3.7 gdb调试
使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。 默认跟踪父进程。 set follow-fork-mode child 命令设置gdb在fork之后跟踪子进程。 set follow-fork-mode parent 设置跟踪父进程。 注意,一定要在fork函数调用之前设置才有效。 【follow_fork.c】
1.4 exec函数族
fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
其实有六种以exec开头的函数,统称exec函数:
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[]);(只有它是系统调用)
1.4.1 execlp函数
加载一个进程,借助PATH环境变量 int execlp(const char *file, const char *arg, ...); 成功:无返回;失败:-1 参数1:要加载的程序的名字。该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数1则出错返回。 该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> int main(int argc,char *argv[]){ pid_t pid =fork(); if(pid==-1){ perror("fork error"); exit(1); } else if(pid ==0 ) { //execlp("ls","ls","-l","-d","-h",NULL); execlp("date","date",NULL); perror("exec error"); exit(1); }else if(pid >0) { sleep(1); printf("I'am parent :%d\n",getpid()); } }
1.4.2 execl函数
加载一个进程, 通过 路径+程序名 来加载。 int execl(const char *path, const char *arg, ...); 成功:无返回;失败:-1 对比execlp,如加载"ls"命令带有-l,-F参数 execlp("ls", "ls", "-l", "-F", NULL); 使用程序名在PATH中搜索。 execl("/bin/ls", "ls", "-l", "-F", NULL); 使用参数1给出的绝对路径搜索。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> int main(int argc,char *argv[]){ pid_t pid =fork(); if(pid==-1){ perror("fork error"); exit(1); } else if(pid ==0 ) { //execlp("ls","ls","-l","-d","-h",NULL); // execlp("date","date",NULL); // execl("./a.out","./a.out",NULL); //execl("./a.out","./a.out",AA,BB,CC); execl("/bin/ls","ls","-l",NULL); perror("exec error"); exit(1); }else if(pid >0) { sleep(1); printf("I'am parent :%d\n",getpid()); } } ~ ~ ~
1.4.3 execvp函数
加载一个进程,使用自定义环境变量env int execvp(const char *file, const char *argv[]); 变参形式: ①... ② argv[] (main函数也是变参函数,形式上等同于 int main(int argc, char *argv0, ...)) 变参终止条件:① NULL结尾 ② 固参指定 execvp与execlp参数形式不同,原理一致。 练习:将当前系统中的进程信息,打印到文件中。 【exec_ps.c】
... char *argv[]={"ls","-l","-h",NULL} execvp("ls",argv); ...
1.4.4 exec函数族一般规律
exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror()和exit(),无需if判断。
l (list) 命令行参数列表 p (path) 搜素file时使用path变量 v (vector) 使用命令行参数数组 e (environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量
事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
exec函数族
1.5 回收子进程
1.5.1孤儿进程
孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。
【orphan.c】 1 2 #include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main(void){ pid_t pid; pid= fork(); if(pid == 0){ while(1){ printf("i am child, my parent pid = %d\n",getppid()); sleep(1); } }else if(pid > 0){ printf("i am parent ,my pid is = %d\n",getpid()); sleep(9); printf("----------parent going to die ------------\n"); }else{ perror("fork"); return 1; } }
1.5.2 僵尸进程
僵尸进程: 进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
特别注意,僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。思考!用什么办法可清除掉僵尸进程呢?杀死父进程
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main(){ pid_t pid; pid =fork(); if(pid == 0){ printf("----child,my parent =%d,going to sleep 10s\n",getppid()); sleep(10); printf("-----------------child die ----------------\n"); }else if(pid > 0) { while(1){ printf("i am parent ,pid = %d,myson = %d\n",getpid(),pid); sleep(1); } }else{ perror("fork"); return 1; } }
1.5.3 wait函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能: ① 阻塞等待子进程退出 ② 回收子进程残留资源 ③ 获取子进程结束状态(退出原因)。 pid_t wait(int *status); 成功:清理掉的子进程ID;失败:-1 (没有子进程)
当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 2.
释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号) 可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
1. WIFEXITED(status) 为非0 → 进程正常结束 WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数) 2. WIFSIGNALED(status) 为非0 → 进程异常终止 WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。 *3. WIFSTOPPED(status) 为非0 → 进程处于暂停状态 WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。 WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> int main(void){ pid_t pid,wpid; int status; pid = fork(); if(pid == 0){ printf("---child ,my id =%d,going to sleep 10s\n",getpid()); sleep(10); printf("----------------child die---------------\n"); return 73; }else if (pid > 0){ //wpid = wait(NULL); wpid = wait(&status); if(wpid == -1){ perror("wait error"); exit(1); } if(WIFEXITED(status)){ printf("child exit with %d\n",WEXITSTATUS(status)); } if (WIFSIGNALED(status)){ printf("child kill with signal%d\n",WTERMSIG(status)); } printf("-----------parent wait finish :%d\n",wpid); }else{ perror("fork"); return 1; } return 0; }
1.5.4waitpid函数
作用同wait,但可指定pid进程清理,可以不阻塞。 pid_t waitpid(pid_t pid, int *status, in options); 成功:返回清理掉的子进程ID; 失败:-1(无子进程) 特殊参数和返回情况: 参数pid: > 0 回收指定ID的子进程 -1 回收任意子进程(相当于wait) 0 回收和当前调用waitpid一个组的所有子进程 < -1 回收指定进程组内的任意子进程 返回0: 参3为WNOHANG,且子进程正在运行。
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。 【waitpid.c】
一次调用,回收一个子进程
#include <string.h> #include <unistd.h> #include <sys/wait.h> #include <pthread.h> int main(int argc,char *argv[]) { int i; pid_t pid,wpid,tmpid; for(i=0;i<5;i++){ pid =fork(); if(pid==0) { break; } if(i == 2){ tmpid = pid; printf("------------pid = %d\n",tmpid); } } if(5==i){ //sleep(5); //wait(NULL); //wpid = waitpid(-1,NULL,WNOHANG); //wpid = waitpid(tmpid,NULL,0); printf("i am parent,before waitpid ,pid =%d\n",tmpid); wpid = waitpid(tmpid,NULL,WNOHANG); if(wpid == -1){ perror("waitpid error"); exit(1); } printf("i am parent ,wait a child finish : %d\n",wpid); }else{ sleep(i); printf("i'm %dth child,pid =%d\n",i+1,getpid()); } return 0; }
想回收多个。while
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <pthread.h> int main(int argc,char *argv[]){ int i; pid_t pid,wpid; for(i=0;i<5;i++){ pid = fork(); if(pid == 0){ break; } } if(5==i){ /* * * while((wpid = waitpid(-1,NULL,0))){ printf("wait child %d \n",wpid); } * */ while((wpid = waitpid(-1,NULL,WNOHANG))!=-1){ if(wpid>0){ printf("wait child %d\n",wpid); } else if(wpid == 0) { sleep(1); continue; } } }else{ sleep(i); printf("i'm %dth child ,pid = %d\n",i+1,getpid()); } }
作业:父进程fork 3 个子进程,三个子进程一个调用ps命令, 一个调用自定义程序1(正常),一个调用自定义程序2(会出段错误)。父进程使用waitpid对其子进程进行回收。