进程原语
1.fork
#include<unistd.h> pid_t fork(void);
fork
子进程复制父进程,子进程和父进程的PID是不一样的,在克隆pcb时,pid没有复制,fork还有底层的函数,如creat(),clone(),retrun 返回。子进程执行的第一条语句是return。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main(void) { pid_t pid; int n=10; //调用一次返回两次,在父进程返回子进程的PID,在子进程返回0; pid=fork();//父和子都存在了 if(pid>0) { /*in parent*/ while(1){ printf("I am parent\n",n++); printf("my pid="%d\n",getpid()); printf("my parent pid="%d\n",getppid()); sleep(1); } } else if(pid==0) { /*in child*/ while(1) { printf("I am ch\n",n++); printf("my pid="%d\n",getpid()); printf("my parent pid="%d\n",getppid()); sleep(1); } } else { perror("fork"); exit();//创建进程失败 } return 0; } //读时共享,写时复制,只读时通过虚拟地址映射到同一物理地址,只有进行写操作时才拷贝一份物理地址,这样不会造成物理地址的浪费。
进程相关函数:
#include<sys/types.h> #include<unistd.h> pid_t getpid(void);//返回调用进程的PID pid_t getppid(void);//返回调用进程的PID uid_t getuid(void);//返回实际用户ID uid_t geteuid(void);//返回有效用户ID gid_t getgid(void);//返回实际用户组ID gid_t getegid(void);//返回有效用户ID
sudo chmod 04755 文件名
如passwd命令,可用ls -l 查看其权限位,其在执行时候,临时身份变为了root。
vfork
- 用于fork后马上调用exec函数
- 父子进程,共用同一地址空间,子进程如果没有马上exec而是修改了父进程出得到量值,此修改会在父进程中生效
- 设计初衷,提高系统效率,减少不必要的开销
- 现在fork已经具备读时共享写时复制机制,vfork逐渐废弃。
2.exec族
#include <unistd.h> int execl(const char *path, const char *arg, ...) int execv(const char *path, char *const argv[]) int execle(const char *path, const char *arg, ..., char *const envp[]) int execve(const char *path, char *const argv[], char *const envp[]) int execlp(const char *file, const char *arg, ...) int execvp(const char *file, char *const argv[])
//l参数列表形式,v数组型,e环境变量数组,p绝对路径
函数返回值:成功 -> 函数不会返回,出错 -> 返回-1,失败原因记录在error中。
这6 个函数在函数名和使用语法的规则上都有细微的区别,下面就可执行文件查找方式、参数表传递方式及环境变量这几个方面进行比较说明。
① 查找方式:上表其中前4个函数的查找方式都是完整的文件目录路径,而最后2个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动从环境变量“$PATH”所指出的路径中进行查找。
② 参数传递方式:exec函数族的参数传递有两种方式,一种是逐个列举的方式,而另一种则是将所有参数整体构造成指针数组进行传递。
在这里参数传递方式是以函数名的第5位字母来区分的,字母为“l”(list)的表示逐个列举的方式,字母为“v”(vertor)的表示将所有参数整体构造成指针数组传递,然后将该数组的首地址当做参数传给它,数组中的最后一个指针要求是NULL。读者可以观察execl、execle、execlp的语法与execv、execve、execvp的区别。
③ 环境变量:exec函数族使用了系统默认的环境变量,也可以传入指定的环境变量。这里以“e”(environment)结尾的两个函数execle、execve就可以在envp[]中指定当前进程所使用的环境变量替换掉该进程继承的所以环境变量。
(4)PATH环境变量说明
PATH环境变量包含了一张目录表,系统通过PATH环境变量定义的路径搜索执行码,PATH环境变量定义时目录之间需用用“:”分隔,以“.”号表示结束。PATH环境变量定义在用户的.profile或.bash_profile中,下面是PATH环境变量定义的样例,此PATH变量指定在“/bin”、“/usr/bin”和当前目录三个目录进行搜索执行码。
PATH=/bin:/usr/bin:.
export $PATH
(5)进程中的环境变量说明
在Linux中,Shell进程是所有执行码的父进程。当一个执行码执行时,Shell进程会fork子进程然后调用exec函数去执行执行码。Shell进程堆栈中存放着该用户下的所有环境变量,使用execl、execv、execlp、execvp函数使执行码重生时,Shell进程会将所有环境变量复制给生成的新进程;而使用execle、execve时新进程不继承任何Shell进程的环境变量,而由envp[]数组自行设置环境变量。
(6)exec函数族关系
第4位统一为:exec
第5位
l:参数传递为逐个列举方式
execl、execle、execlp
v:参数传递为构造指针数组方式
execv、execve、execvp
第6位
e:可传递新进程环境变量
execle、execve
p:可执行文件查找方式为文件名
execlp、execvp
事实上,这6个函数中真正的系统调用只有execve,其他5个都是库函数,它们最终都会调用execve这个系统调用。
exec函数族
应用举例:
char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL}; char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL}; execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL); execv("/bin/ps", ps_argv); execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp); execve("/bin/ps", ps_argv, ps_envp); execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL); execvp("ps", ps_argv);//常用
exec
exec加载一个程序,替换掉APP的代码段,堆,栈。
#include<stdio.h> #include<unistd.h> int main(void) { printf("hello\n"); execl("/bin/ls","ls","-l",NULL); //后边这一句不会再执行了 printf("world\n"); return 0; }
fork和exec结合使用
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main(void) { pid_t pid; pid=fork(); if(pid==0) { execl("/usr/bin/firefox","firefox","www.baidu.com",NULL); } else if(pid>0) { while(1) { printf("I am parent\n"); sleep(1); } } else { perror(fork"); exit(1); }
shell的工作原理就是fork和exec结合使用,如上图所示。
exec练习:
/* upper.c */ #include <stdio.h> int main(void) { int ch; while((ch = getchar()) != EOF) { putchar(toupper(ch)); } return 0; }
/* wrapper.c */ #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <fcntl.h> int main(int argc, char *argv[]) { int fd; if (argc != 2) { fputs("usage: wrapper file\n", stderr); exit(1); } fd = open(argv[1], O_RDONLY); if(fd<0) { perror("open"); exit(1); } dup2(fd, STDIN_FILENO); close(fd); execl("./upper", "upper", NULL); perror("exec ./upper"); exit(1); }
执行过程
3.wait/waitpid
僵尸进程:如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理(用户空间被释放而pcb并没有释放,等父进程回收),这样的进程状态称为僵尸进程。任何进程在刚终止事都是僵尸进程。
孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为1号进程init进程,称为init进程领养孤儿进程。
wait
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int *status);//wait是一个阻塞函数,等待回收子进程资源,如果没有子进程,wait返回-1; pid_t waitpid(pit_t pid,int *status,int options);
pit_t pid:
<-1:回收指定进程组内的任意子进程
-1:回收任意子进程
0:回收和当前调用waitpid一个组的所有子进程
>0:回收指定ID的子进程
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:
pid = wait(NULL);
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1(如父进程没有子进程),同时errno被置为ECHILD。
如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中, 这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息 被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦。如上图中的信号值部分。人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:
1,WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。
(请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数–指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了。)
2, WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。
进程组:当父进程创建出子进程,子进程默认和父进程一个组,可以一次杀死一个进程组中的所有进程 kill -9 -进程号。子进程仍然可以产生起的的子进程成为新的进程组。