多进程
- 一 并行和并发的区别
- 二 进程控制块PCB
- 三 进程号
- 四 进程的状态
- 五 进程号,父进程号,进程组号相关函数
- 六 fork创建进程
- 七 特殊的进程(孤儿进程,僵尸进程,守护进程)
- 八 父进程回收子进程资源
- 九 wait函数
- 十 waitpid函数
- 十一 循环创建子进程存在的问题
- 十二 正确的循环创建多个子进程的方法是防止子进程再创建孙子进程
- 十三 循环创建多个子进程模板
- 十四 终端
- 十五 查看文件描述符对应的文件名函数ttyname
- 十六 进程组
- 十七 会话
- 十八 创建守护进程模型
- 十九 vfork函数创建子进程
- 二十 exec函数族
- 二十一 vfork和exec配合使用,(vfork一定要和exec一起使用才有意义,vfork会保证vfork子进程先运行,子进程执行到exec函数,由于exec函数要覆盖调用它的进程,而调用它的进程是父和子公用内存空间,它不能覆盖父进程,所以只能新建一块内存空间来运行exec启动的程序)
- 二十二 vfork+exec和fork+exec的区别(vfork会保证exec调用的外部程序比vfork父进程先执行)
一 并行和并发的区别
- 并发和并行都是多个任务同时执行
- 并行:指同一时刻,有多条指令在多个处理器上同时执行。(多核)
- 并发:是时分复用一个处理器。
二 进程控制块PCB
进程运行时,内核为每个进程分配一个进程控制块(PCB),用来维护进程的相关信息。Linux内核的进程控制块是task_struct结构体。
三 进程号
- 进程号PID:类型为pid_t(非负整数),进程号范围0-32767
- 父进程号PPID:
- 进程组号PGID:是一个或多个进程的集合,组中成员可以接收同一终端的各种信号。默认情况下,当前进程号就是进程的进程组号
四 进程的状态
五 进程号,父进程号,进程组号相关函数
5.1 getpid获取本进程的进程号函数
- 需要的头文件和函数原型
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
- 返回值:本进程号
- 代码
#include <sys/types.h>
#include <stdio.h>
#include<unistd.h>
int main(int arg,char *args[])
{
pid_t num = getpid();
printf("%d\n",num);
}
5.2 getppid获取父进程号函数
- 需要的头文件和函数原型
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
- 返回值:父进程号
- 代码
#include <sys/types.h>
#include <stdio.h>
#include<unistd.h>
int main(int arg,char *args[])
{
pid_t num = getppid();
printf("%d\n",num);
}
5.3 getpgid获取进程组号函数
- 需要的头文件和函数原型
#include <sys/types.h>
#include <unistd.h>
pid_t getpgid(pid_t pid);
- 参数:进程号
- 返回值:参数为0时返回当前进程的进程组号;否则返回参数指定的pid的进程组号
- 代码
#include <sys/types.h>
#include <stdio.h>
#include<unistd.h>
int main(int arg,char *args[])
{
pid_t pid = getpid();
pid_t ppid = getppid();
pid_t pgid1 = getpgid(0);
pid_t pgid2 = getpgid(ppid);
printf("%d\n",pid);
printf("%d\n",ppid);
printf("%d\n",pgid1);
printf("%d\n",pgid2);
}
六 fork创建进程
6.1 fork函数介绍
- 需要的头文件和函数原型
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
- 功能:创建子进程
- 返回值:
成功:子进程中返回0;父进程中返回子进程的pid
失败:返回-1
失败有两个原因:
1)进程号耗尽,此时errno被设置为EAGAIN
2)系统内存不足,此时errno被设置为ENOMEM
- fork出来的子进程和父进程的关系
1)使用fork函数得到的子进程是父进程的一个复制品,它从父进程继承了整个进程的地址空间。
2)地址空间:包括进程上下文、进程堆栈、打开的文件描述符、信号控制设置、进程优先级、进程组号等。
3)子进程独有的只有他的进程号、计时器等。
4)因此使用fork函数的代价是很大的。
6.2 父进程和子进程都是从fork后开始继续执行的
6.3 父进程和子进程是同时运行的,而且内存空间独立,子进程复制了父进程的所有内存空间,谁先运行不确定
七 特殊的进程(孤儿进程,僵尸进程,守护进程)
7.1 孤儿进程(无危害)
父进程先结束,子进程就是孤儿进程,会被1号进程接管,1号进程负责给孤儿进程回收资源
7.2 僵尸进程
子进程结束,但父进程没有回收子进程的资源(PCB资源),子进程就是僵尸进程
#include <sys/types.h>
#include <stdio.h>
#include<unistd.h>
int main(int arg,char *args[])
{
int num = 10;
pid_t pid = fork();
//fork失败
if(pid < 0)
{
perror("fork失败\n");
return 0;
}
//父进程
else if(pid > 0)
{
printf("我是父进程%d\n",getpid());
while(1)
;
}
//子进程
else if(pid == 0)
{
printf("我是子进程%d\n",getpid());
}
return 0;
}
7.3 守护进程
是脱离终端的孤儿进程,在后台运行,为特殊服务存在的(一般用于服务器)
八 父进程回收子进程资源
1. 在每个进程退出的时候,内核释放该进程的所有资源,包括打开的文件,占用的内存等。但任然为其保留了一定的信息,这些保留的信息主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等);
2. 父进程可以通过调用wait或waitpid得到子进程的退出状态,同时彻底清除这个进程;
3. 注意:一次wait或waitpid调用只能清理一个子进程,要清理多个子进程需要循环。
4. 一般父进程只做回收子进程资源这一个任务。
九 wait函数
- 需要的头文件和函数原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
- 功能:等待任意一个子进程接收,然后回收该子进程资源
- 参数:
wstatus:进程退出时的状态信息。
如果wstatus不是NULL,wait()会把子进程的退出状态存放到wstatus中,这个值是一个int型的。
1)先用宏WIFEXITED(wstatus)查看子进程是否是正常终止,如果是正常终止得到非零值。
2)在子进程是正常终止的前提下才能用宏WEXITSTATUS(wstatus)来得到子进程的退出状态,退出状态是保存在wstatus变量的8-16位中的
- 返回值
成功:已经结束的子进程pid
失败:-1
- 注意
1)调用wait函数的进程会阻塞,直到他的一个子进程结束或收到一个不能被忽视的信号后才继续往下面的代码执行。
2)如果调用wait函数的进程没有子进程,wait()会立即返回。
3)如果调用wait函数之前已经有一个该进程的子进程结束,那么wait()也立即返回,并且回收他的那个早已结束的子进程的资源。
- 代码
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
int main(int arg,char *args[])
{
int num = 10;
pid_t pid = fork();
//fork失败
if(pid < 0)
{
perror("fork失败\n");
return 0;
}
//父进程
else if(pid > 0)
{
printf("我是父进程%d\n",getpid());
int status = 0;
pid_t pid = wait(&status);
printf("子进程%d退出了!",pid);
printf("a=%d\n",WIFEXITED(status));
printf("b=%d\n",WEXITSTATUS(status));
}
//子进程
else if(pid == 0)
{
printf("我是子进程%d\n",getpid());
for(int i=5;i>0;i--)
{
printf("子进程%d还是%d秒生命\n",getpid(),i);
sleep(1);
}
exit(100);
}
return 0;
}
十 waitpid函数
- 需要的头文件和函数原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
-
功能
等待子进程结束,如果子进程结束了,此函数回收子进程资源 -
参数
pid:
pid >0: 等待进程id等于pid的子进程;
pid =0:等待同一进程组中任何子进程,如果子进程已经加入其它进程组,则waitpid不会等待他;
pid =-1:等待任意子进程,此时waitpid和wait作用一样;
pid <-1: 等待指定进程组中任意子进程,这个进程组id等于pid的绝对值
wstatus:子进程退出时的状态信息,和wait中用法一样
options:提供一些额外选项来控制waitpid
0:同wait函数,阻塞父进程,等待子进程结束。
WNOHANG:如果没有任何已经结束的子进程则返回。
WUNTRACED:如果子进程暂停,则马上返回。
- 返回值分三种情况
1)正常返回时,返回已回收子进程的pid。
2)如果设置了选项WNOHANG,而调用中waitpid还有子进程在运行且没有子进程退出返回0;父进程所有子进程都已经退出返回-1;返回>0表示等到了一个子进程退出。
3)如果调用中出错,返回-1,这是errno会设置成相应的值以指示错误。
十一 循环创建子进程存在的问题
当循环创建多个子进程时,由于子进程还创建了孙子进程,所以生成的子进程数量比预期的多
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include<unistd.h>
int main(int arg,char *args[])
{
for(int i=0;i<2;i++)
{
pid_t pid = fork();
//fork失败
if(pid < 0)
{
perror("fork失败\n");
return 0;
}
//父进程
else if(pid > 0)
{
printf("我是父进程%d,我生的子进程是%d\n",getpid(),pid);
}
//子进程
else if(pid == 0)
{
printf("我是子进程%d\n",getpid());
}
}
return 0;
}
十二 正确的循环创建多个子进程的方法是防止子进程再创建孙子进程
十三 循环创建多个子进程模板
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#define N 5 //创建5个子进程
int main(int arg,char *args[])
{
int i = 0;
for(i=0;i<N;i++)
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
return 0;
}
if(pid == 0) //防止子进程创建孙子进程
break;
}
//判断具体的子进程
if(i==0) //子进程1
{
//负责完成任务1
printf("我是第%d个子进程%d,我的父进程是%d\n",i,getpid(),getppid());
}
else if(i==1) //子进程2
{
printf("我是第%d个子进程%d,我的父进程是%d\n",i,getpid(),getppid());
}
else if(i==2) //子进程3
{
printf("我是第%d个子进程%d,我的父进程是%d\n",i,getpid(),getppid());
}
else if(i==3) //子进程4
{
printf("我是第%d个子进程%d,我的父进程是%d\n",i,getpid(),getppid());
}
else if(i==4) //子进程5
{
printf("我是第%d个子进程%d,我的父进程是%d\n",i,getpid(),getppid());
}
else if(i==N) //父进程
{
//父进程只负责回收所有子进程的资源
while(1)
{
pid_t pid = waitpid(-1,NULL,WNOHANG);
if(pid>0) //某个子进程退出了
{
printf("子进程%d退出了\n",pid);
}
else if(pid == 0)//还没有子进程在运行
{
continue;
}
else if(pid == -1)//所有子进程都退出了
{
break;
}
}
}
return 0;
}
十四 终端
在UNIX系统中,用户通过终端登录系统后得到一个shell进程,这个终端成为shell进程的控制终端(Controlling Terminal
),进程中控制终端是保存在PCB中的信息,而fork会复制PCB中的信息,因此由shell进程启动的其他进程的控制终端也是这个终端。默认情况下,每个进程的标准输入、标准输出和标准错误输出都指向控制终端。
情况一 程序没有子进程:
- 执行a.out时,bash进程exec子进程a.out并移交终端控制权给子进程a.out
- a.out运行结束将终端控制权还给bash进程
情况二 程序有子进程:
- 执行a.out时,bash进程exec子进程a.out并移交终端控制权给子进程a.out
- a.out fork出他自己的子进程,并把终端控制权也给了他的子进程。a.out和子进程同时有终端控制权
3a. 如果a.out父进程运行结束,将终端控制权还给bash进程,这时a.out子进程没有终端的输入权限,只有终端的输出权限。
3b. 如果a.out子进程运行结束,a.out不会将终端控制权还给bash进程,这时a.out父进程仍然有终端的输入输出权限。
十五 查看文件描述符对应的文件名函数ttyname
- 需要的头文件和函数原型
#include <unistd.h>
char *ttyname(int fd)
-
功能
由文件描述符查出对应的文件名 -
参数
fd:文件描述符 -
返回值
成功:终端名
失败:NULL -
实例
int main(int arg,char *args[])
{
printf("标准输入的设备文件是:%s\n",ttyname(0));
printf("标准输出的设备文件是:%s\n",ttyname(1));
printf("标准错误输出的设备文件是:%s\n",ttyname(2));
}
十六 进程组
进程组:也称为作业,代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid函数和kill函数的参数都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。当父进程创建子进程时,默认子进程与父进程属于同一进程组。进程组id为第一个进程id(组长进程)。所以组长进程标识:其进程组id为其进程id
可以使用kill -进程组id(负的)来将整个进程组内的进程全部杀死
进程组id为第一个进程id(组长进程);
进程id和进程组id相同的进程就是组长进程;
组长进程可以创建一个进程组,创建进程组中的进程,然后终止。只有进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。进程组的生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。一个进程可以为自己或子进程设置进程组id。
16.1 获取进程组id函数getpgrp
- 需要的头文件和函数原型
#include<unistd.h>
pid_t getpgrp(void);
-
函数功能
获取当前进程的进程组id -
返回值
返回调用者的进程组id
16.2 获取指定进程的进程组id
- 需要的头文件和函数原型
pid_t getpgid(pid_t pid);
-
功能
获取指定进程的进程组id -
参数
pid:进程号,如果pid=0,那么和getpgrp函数一样 -
返回值
成功:进程组id
失败:-1
16.3 改变进程组函数setpgid
-
函数原型
int setpgid(pid_t pid, pid_t pgid); -
功能
改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新进程组 -
参数
-
返回值:
成功:0
失败:-1
十七 会话
17.1 会话介绍
1. 会话是一个或多个进程组的集合。一个会话可以有一个控制终端。这通常是终端设备或伪终端设备;建立与控制终端连接的会话首进程称为控制进程;一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组;如果一个会话有一个控制终端,则他有一个前台进程组,其他进程组为后台进程;如果终端接口检测到断开连接,则将挂断信号发送至控制进程(会话首进程)。
2. 如果进程id=进程组id=会话id,那么该进程为会话首进程
17.2 创建会话的步骤
- 调用进程不能是进程组的组长进程,该进程变成新会话首地址(session header)
- 该调用进程是组长进程则出错
- 该进程成为一个新进程组的组长进程
- 需有root权限(ubuntu不需要)
- 新会话丢弃原有的控制终端,该会话没有控制终端
- 建立新会话时,先调用fork,父进程终止,子进程调用setsid
17.3 获取进程所属会话id函数getsid
- 需要的头文件和函数原型
#include<unistd.h>
pid_t getsid(pid_t pid);
- 功能
获取进程所属会话的id - 参数
pid:进程号,pid为0表示查看当前进程session id - 返回值
成功:返回调用进程的会话id
失败:-1
17.4 创建会话函数getsid
- 需要的头文件和函数原型
#include<unistd.h>
pid_t setsid(void);
-
功能
创建一个会话,并以自己的id设置进程组id,同时也是新会话的id。调用了setsid函数的进程,即是新的会长,也是新的组长 -
参数
-
返回值
成功:返回调用进程的会话id
失败:-1
17.5 创建一个会话实例
#include <stdio.h>
#include <unistd.h>
int main(int arg,char *args[])
{
pid_t pid = fork();
//父进程结束
if(pid > 0)
_exit(0);
//子进程设置会话
else if(pid == 0)
setsid();
printf("进程id:%d\n",getpid());
while(1)
;
return 0;
}
十八 创建守护进程模型
#include <stdio.h>
#include<sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
int main(int arg,char *args[])
{
pid_t pid = fork();
//父进程结束
if(pid > 0)
_exit(0);
//子进程设置会话
int res = setsid();
if(res == -1)
perror("setsid");
//改变工作目录(非必需)
chdir("/");
//设置权限掩码
umask(0002);
//关闭文件描述符012
close(1);
close(2);
close(0);
//守护进程的核心任务
while(1)
{
sleep(1);
printf("守护进程的pid为%d\n",getpid());
}
return 0;
}
十九 vfork函数创建子进程
- 需要的头文件和函数原型
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
- 功能
和fork函数一样,都是在已有的进程中创建一个子进程,但fork和vfork创建出来的子进程是有区别的 - 返回值
失败:返回-1
成功:子进程返回0;父进程返回子进程的pid
- 特点
a. vfork创建的子进程和父进程共享同一块内存空间,只是pid,ppid等不同;
b. vfork会保证子进程先运行,只有子进程退出了或调用了exec族函数后,父进程才能运行;
二十 exec函数族
20.1 exec函数族实现的功能
在进程内部启动一个外部程序
20.2 exec函数族中包括一个系统调用的原始函数execve和六个封装execve而得的库函数
- 需要的头文件
#include <unistd.h>
- 原始系统调用函数execve的原型
int execve(const char *filename, char *const argv[], char *const envp[]);
- 六个封装后的库函数原型
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[]);
- 函数名中字母的代表含义
l(list): 参数地址列表,以空指针结尾。参数地址列表char *arg0,char *arg1,...,char *argn, NULL
v(vector): 存有各参数地址的指针数组的地址。使用时先构造一个指针数组,指针数组存各参数的地址,然后将该指针数组地址作为函数的参数。
p(path): 按PATH环境变量指定的目录搜索可执行文件。以p结尾的exec函数取文件名作为参数。当指定filename作为参数时,若filename中包含/, 则将其视为路径名,并直接到指定的路径中执行程序。
e(environment): 存有环境变量字符串地址的指针数组的地址。execle和execve改变的是exec函数启动的程序的环境变量(新的环境变量完全由environment指定),其他四个exec函数启动的程序则使用默认系统环境变量
20.3 exec成功,不会执行exec之后的代码
#include <stdio.h>
#include<sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
int main(int arg,char *args[])
{
printf("调用pwd前\n");
int res = execl("/bin/pwd","pwd",NULL);
if(res == -1)
perror("execl");
else
printf("res=%d\n",res);
printf("调用pwd后\n");
return 0;
}
20.4 exec失败,会执行exec之后的代码
#include <stdio.h>
#include<sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
int main(int arg,char *args[])
{
printf("调用pwd前\n");
int res = execl("/bin/pwd1","pwd",NULL);
if(res == -1)
perror("execl");
else
printf("res=%d\n",res);
printf("调用pwd后\n");
return 0;
}
20.5 exec成功后exec执行的进程会覆盖调用进程的内存中的数据段、代码段、和堆栈段,只保留了进程id、父进程id、进程组id、控制终端、根目录、当前工作目录、进程信号屏蔽集、未处理信号
20.6 其他几个exec函数的用法
#include <stdio.h>
#include<sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
int main(int arg,char *args[])
{
printf("调用ls前\n");
//int res = execl("/bin/ls","ls","-a",NULL);
//int res = execlp("ls","ls","-a",NULL);
char *avg[] = {"ls","-a",NULL};
int res = execvp("ls",avg);
if(res == -1)
perror("execl");
else
printf("res=%d\n",res);
printf("调用test后\n");
return 0;
}
二十一 vfork和exec配合使用,(vfork一定要和exec一起使用才有意义,vfork会保证vfork子进程先运行,子进程执行到exec函数,由于exec函数要覆盖调用它的进程,而调用它的进程是父和子公用内存空间,它不能覆盖父进程,所以只能新建一块内存空间来运行exec启动的程序)
#include <stdio.h>
#include<sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
int main(int arg,char *args[])
{
pid_t pid = vfork();
if(pid == 0)//子进程
{
char *avg[] = {"test",NULL};
//子进程负责启动另一个程序
int res = execvp("test",avg);
if(res == -1)
perror("execl");
else
printf("res=%d\n",res);
}
else if(pid >0 ) //父进程
{
//父进程运行自己的程序
while(1)
{
printf("父进程中,pid=%d\n",getpid());
sleep(1);
}
}
return 0;
}
二十二 vfork+exec和fork+exec的区别(vfork会保证exec调用的外部程序比vfork父进程先执行)
- vfork+exec:vfork保证vfork子进程先执行,执行exec调用外部程序后,vfork父进程在执行;
- fork+exec:fork父进程和fork子进程不一定谁先执行。也可能是fork父进程先执行,fork子进程执行exec调用的外部程序后执行;也可能是fork子进程执行exec调用的外部程序先执行,fork父进程后执行。