目录
解决问题:根据 i 的不同,区分,父进程中 i 等于子进程数
一、进程的概述
1、程序和进程的区别:
程序是静态的,存储在磁盘空间
进程是动态的,可以调度、执行、消亡。在内存空间。
2、单道和多道程序设计:
1、单道(被淘汰):所有进程只能一个一个排队执行,后面的必须排队
2、多道:内存中存放多道独立运行的程序,在管理程序的控制下,能够穿插运行。
时钟中断作为理论基础,并发时,任何进程都不愿意放弃cpu,所以需要一种强制让进程让出cpu资源的手段。
3、并行和并发的区别
都是指多个任务同时执行
并行(多核):在微观上,同一时刻,有多条指令在多个处理器上同时执行。
并发(单核):同一时刻只能有一条指令执行,但是因为快速轮转,在宏观上具有多个进程同时执行的效果。
4、进程控制块PCB
1、程序运行的时候,内核会为每进程分配一个PCB,维护进程相关的信息。linux内核的进程控制块是task_struct结构体。在系统内核空间里面。
2、进程的状态有就绪、运行、挂起、停止等状态。进程是分配资源的基本单位。
3、当进程切换时,需要保存和恢复一些cpu寄存器、描述虚拟地址空间的信息、描述控制终端的信息、当前工作目录、umask掩码、文件描述符表、包括很多指向file的指针、和信号相关的信息,包括组id、用户id,会话和进程组,进程可以使用的资源上限。
5、进程的状态
就绪态、执行态、等待态。
1)就绪态:执行条件全部满足,等待cpu的调度。
2)执行态:正在被cpu调度执行。
3)等待态:条件不满足,等待条件满足。
6、进程号
1、pid ppid
2、 pgid:进程组号
进程组是一个或者多个进程的集合,它们之间相互关联,进程组可以收到同一个终端的各种信号,关联的进程有一个进程组号。没有加入进程组的也有个pgid
3、获取进程号的函数getpid
头文件:
#include <sys/types.h>
#include <unistd.h>
功能:获取本进程的pid
原型:pid_t getpid(void);
返回值:本进程号
4、获取父进程号的函数getppid
头文件:
#include <sys/types.h>
#include <unistd.h>
功能:获取父进程的pid
原型:pid_t getppid(void);
返回值:父进程号
5、获取进程的pgid的函数getpgid
头文件:
#include <sys/types.h>
#include <unistd.h>
功能:获取进程的pgid
原型:pid_t getpgid(pid);
返回值:进程的进程组id,如果pid参数为0的时候,返回的就是当前进程进程组号。否则就是指定进程的进程组号。组长pid与组的pgid相同
7、创建子进程
1、pid_t fork(void);
调用就产生进程
头文件:
#include <sys/types.h>
#include <unistd.h>
功能:
从一个已经创建的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。优先执行父进程
返回值:
成功:子进程中返回0,父进程中返回子进程ID。
失败返回-1:
1、进程数已经打到系统上限。
2、系统内存不足。
fork出来的子进程的父进程之间的关系
1)子进程是父进程的复制品,继承了整个进程的地址空间。
2)地址空间:包括地址的上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号。
3)子进程所独有的只有自己的进程号、计时器等。因此、使用fork的代价是很大的。
4)子进程是从父进程的fork之后运行。
案例:使用fork创建子进程
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
pid_t pid = fork();
if( pid < 0)
{
perror("fork");
return -1;
}
else if(pid==0)
{
printf("子进程:pid=%d 它的父进程ppid= %d",getpid(),getppid());
}
else if(pid>0)
{
printf("父进程:pid=%d\n",getpid());
}
getchar();
return 0;
}
运行结果:
使用ps -ef查看
2、vfork创建子进程:
原型:pit_t vfork(void)
功能:创建一个子进程,使用vfork创建的子进程会优先执行子进程
返回值:创建成功子进程中返回0,父进程中返回子进程号id,失败返回-1.
案例:使用vfork创建子进程
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
pid_t pid = vfork();
if( pid < 0)
{
perror("fork");
return -1;
}
else if(pid==0)
{
for(int i =5;i>0;i--)
{
printf("子进程%d i=%d\n",getpid(),i);
sleep(1);
}
_exit(-1);
}
else if(pid>0)
{
for(int i =5;i>0;i--)
{
printf("父进程%d i=%d\n",getpid(),i);
sleep(1);
}
}
return 0;
}
运行结果:
分析:子进程是先运行,只有当子进程结束的时候,在运行父进程,因为子进程和父进程使用同一块空间。
验证分析:
int main(int argc, char const *argv[])
{
int num=10;
pid_t pid = vfork();
if( pid < 0)
{
perror("vfork");
return -1;
}
else if(pid==0)
{
num =1000;
printf("子进程%d num=%d\n",getpid(),num);
_exit(1);
}
else if(pid>0)
{
printf("父进程%d num=%d\n",getpid(),num);
}
return 0;
}
8、特殊的进程
孤儿进程、僵尸进程、守护进程。
1)孤儿进程(无危害):
父进程先结束、子进程还在运行,这个子进程就是孤儿进程,会被1号进程接管,由一号进程负责给子进程回收资源。
案例:
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
pid_t pid = fork();
if( pid < 0)
{
perror("fork");
return -1;
}
else if(pid==0)
{
while(1)
{
printf("子进程:pid=%d 它的父进程ppid= %d\n",getpid(),getppid());
sleep(1);
}
}
else if(pid>0)
{
printf("父进程将在三秒后结束\n");
sleep(3);
}
return 0;
}
运行结果:
分析:当父进程结束,但是子进程还在运行,子进程会被1号进程收养,同时脱离终端显示,此时只有使用kill杀死。
2)僵尸进程
1、子进程结束,父进程没有回收资源(PCB),子进程就是僵尸进程。
案例:
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
int main(int argc, char const *argv[])
{
pid_t pid = fork();
if( pid < 0)
{
perror("fork");
return -1;
}
else if(pid==0)
{
printf("pid=%d ,ppid=%d",getpid(),getppid());
return -1;
}
else if(pid>0)
{
printf("父进程");
while(1);
}
return 0;
}
运行结果:
分析:当子运行完毕,但是父进程还在被while(1)阻塞,没有回收子进程资源,所以子进程成为僵尸进程 defunct:僵尸进程
3)守护进程
是脱离终端的孤儿进程 ,在后台运行,为特殊服务存在,一般用于服务器。
创建守护进程:
1)创建子进程,父进程必须退出,所有工作在子进程中进行形式上脱离了终端。
2)在子进程中创建新会话(必须) setsid()函数使子进程完全独立出来,脱离控制
3)改变当前目录为根目录(不是必须) chdir()函数防止占用可卸载的文件系统也可以换成其它路径
4)重设文件权限掩码(不是必须)umask()函数防止继承的文件创建屏蔽字拒绝某些权限增加守护进程灵活性
5)关闭文件描述符(不是必须)继承的打开文件不会用到,浪费系统资源,无法卸载6)开始执行守护进程核心工作(必须)守护进程退出处理程序模型
案例:
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include<sys/wait.h>
#include <sys/stat.h>
int main(int argc, char const *argv[])
{
pid_t pid = fork();
//父进程退出
if(pid>0)
{
_exit(-1);
}
setsid(); //创建新会话
chdir("/");//改变当前目录为根目录
umask(0002);//修改文件权限掩码
//关闭文件描述符
close(0);
close(2);
close(1);
while(1)
{ //创建守护任务
;
}
return 0;
}
9、父进程回收子进程资源
父进程可以调用wait或waitpid得到它的退出状态同时彻底清楚掉这样的进程。
每个进程结束的时候,内核释放该进程的所有资源、包括打开的文件等、,但是仍然保留了一定的信息,这些信息主要是指PCB进程控制块
父进程可以是使用wait和waitpid得到它的退出状态已经清除掉这个进程。
wait、waitpid基本都是在父进程中调用
1、wait函数(带阻塞)
头文件:
#include <sys/types.h>
#include <sys/wait.h>
原型: pid_t wait(int *wstatus)
参数:进程退出时候的状态。
功能:等待任意一个子进程结束、如果任意一个子进程结束了,此函数就会回收该进程的资源。
返回值:成功返回结束子进程的pid,失败返回-1,如果没有子进程或者子进程已经结束,那么会立即返回
案例:
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include<sys/wait.h>
int main(int argc, char const *argv[])
{
pid_t pid = fork();
if( pid < 0)
{
perror("fork");
_exit(-1);
}
else if(pid==0)
{
for(int i =0;i<5;i++)
{
printf("子进程:%d还有%ds退出\n",getpid(),i);
sleep(1);
}
printf("子进程:%d退出了\n",getpid());
_exit(-1);
}
else if(pid>0)
{
printf("父进程:%d 准备回收子进程\n",getpid());
wait(NULL);
printf("父进程:%d等到了子进程结束",getpid());
}
return 0;
}
运行结果:
分析:当父进程运行到wait时,会阻塞父进程,去等待子进程结束后,回收资源后再继续执行下面代码。
补充:如果关心状态值
else if(pid>0)
{
int status;
printf("父进程:%d 准备回收子进程\n",getpid());
pid_t pid = wait(&status);
//如果是正常退出
if(WIFEXITED(status))
{
//退出的状态
printf("子进程退出的状态值:%d\n",WEXITSTATUS(status));
}
printf("父进程:%d等到了子进程:%d结束\n",getpid(),pid);
}
会返回退出的状态值_exit里的状态值。
10、创建多进程
1、知识点引入 :创建两个子进程
问题引入:
使用循环创建子进程,为什么创造了三个子进程? 进程数,2的N次方,子进程数2的N次方-1,孙也算子进程。
解决问题:防止子进程创建孙进程
for(int i = 0;i<2;i++)
{
pid_t PID = fork();
if(PID == 0)
{
break;
}
}
问题引入:怎么区分几个进程?
解决问题:根据 i 的不同,区分,父进程中 i 等于子进程数
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include<sys/wait.h>
#define N 2
int main(int argc, char const *argv[])
{
int i = 0;
for( i;i<N;i++)
{
pid_t PID = fork();
if(PID == 0)
{
break;
}
}
if(i==0)
{
printf("这是子进程1:pid=%d,ppid=%d\n",getpid(),getppid());
}
else if(i==1)
{
printf("这是子进程2:pid=%d,ppid=%d\n",getpid(),getppid());
}
else if(i==N)
{
printf("这是父进程3:pid=%d,ppid=%d\n",getpid(),getppid());
}
getchar();
return 0;
}
运行结果:
案例:创建多线程完成提前以及任务数
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include<sys/wait.h>
#define N 2
int main(int argc, char const *argv[])
{
int i = 0;
for( i;i<N;i++)
{
pid_t PID = fork();
if(PID<0)
{
perror("fork");
return -1;
}
if(PID == 0)
{
break;
}
//子进程分别完成任务
}
if(i==0)
{
printf("这是子进程1:pid=%d,ppid=%d\n",getpid(),getppid());
int j=5;
for(j;j>0;j--)
{
printf("子进程1的生命周期还有:%ds\n",j);
sleep(1);
}
return 1;
}
else if(i==1)
{
printf("这是子进程2:pid=%d,ppid=%d\n",getpid(),getppid());
int j=3;
for(j;j>0;j--)
{
printf("子进程2的生命周期还有:%ds\n",j);
sleep(1);
}
return 1;
}
else if(i==N) //回收所有子进程资源
{
printf("这是父进程3:pid=%d\n",getpid());
printf("父进程准备开始回收子进程\n");
while(1)
{
pid_t PID1 = waitpid(-1,NULL,WNOHANG);
if(PID1>0)
{
printf("父进程回收了子进程%d\n",PID1);
}
//还有子进程在运行,再回去扫描
else if(PID1==0)
{
continue;
}
else if (PID1==-1)
{
printf("所有子进程都回收\n");
break;
}
}
}
return 0;
}
运行结果:
同时也印证了OS的多道程序设计
缺点:要在一开始就确定任务个数,不能在中途增加任务。
11、进程的补充
1)终端:
用户在登陆终端的时候,会分配一个shell进程。这个终端成为shell进程的控制终端,控制终端是保存在PCB中的信息。fork会复制PCB中的信息,因此由shell进程启动的其他进程的控制终端也是这个终端。
步骤:
1、bash进程的PCB里面保存了控制终端权限,当你在当前bash进程中创建了一个进程a.out后,子进程a.out会fork bash进程
bash暂时失去对终端的控制权限,而a.out 的PCB是复制的bash的PCB,暂时获得了对终端的控制。
2、当a.out 又fork了 一个子进程后,他们同时获得对终端的控制权限(输入输出)。
3、当a.out和a.out的子进程都结束后,a.out、a.out的子进程失去了对终端的控制权限,但是a.out的子进程遗留了一部分输出权限。
12、进程组
多个进程的集合。
1)当父进程创建子进程的时候,默认父进程和子进程是同一进程组。
2)进程组组长ID和进程组id相同。
3)可以通过kill -SIGKILL -进程组号(是负的)来将整个进程组进程全部杀死。
4)一个进程组只要有一个进程组在,就存在,与组长进程是否存在无关。
一个进程可以为自己和子进程设置进程组。
头文件:
#include <sys/types.h>
#include <unistd.h>
函数原型:pid_t setpgid(pid_t pid ,pid_t pgid)
功能:改变进程默认所属的进程组。通常可用加入一个现有的进程组或者创建一个新的进程组。
返回值:成功返回0,失败返回-1
13、会话
1)会话是一个或者多个进程组的集合。一个会话可以有一个控制终端,建立与控制终端连接的会话首进程为称为控制进程。
2)一个会话中的几个进程组可以被分为,前台进程组合后台进程组。如果一个会话有一个控制终端,则它有一个前台进程组,其他其他进程为后台进程组。
3)如果终端接口检测到断开连接,则将挂断信号发送到控制进程(会话首进程)。
4)如果进程ID==进程组ID==会话ID ,那么这个进程为会话首进程。
创建会话的步骤
1)调用进程不能是进程组组长,该进程变成会话首进程。
2) 该调用进程是组长进程,则返回出错。
3)该进程成为一个新进程组的组长进程
4)需要有root权限,Ubuntu不需要。
5)新会话丢弃原有的控制终端,该会话没控制终端。
6)建立新会话时,先调用fork,父进程终止、子进程调用setsid
函数原型:pid_t setsid(void)
头文件:
#include <sys/types.h>
#include <unistd.h>
功能:创建一个会话,以自己的ID设置进程组ID,同时也是会话ID。
返回值:
成功:返回调用进程的会话ID
失败:返回-1
案例:
int main(int argc, char const *argv[])
{
pid_t pid = fork();
if(pid>0)
{
return -1;
}
else if(pid == 0)
{
int i =setsid();
if(i == -1)
{
perror("setsid");
return -1;
}
}
while(1);
return 0;
}
14、exec函数族
1)这几个里面只有execve里面才是真正意义上的系统调用
2)函数中有 l (list)的就是表明使用列表方式传参,有v(vector) 就是使用指针数组传参
有p表明到系统环境中找可执行文件。
案例1:在代码中使用execl执行ls命令
execl(可执行文件位置,可执行文件名,可执行文件的选项,以NULL结尾)
int main(int argc, char const *argv[])
{
printf("执行前:\n");
execl("/bin/ls","ls","-a","-l","-h",NULL);
printf("执行后:\n");
return 0;
}
分析:没有打印执行后这句话,这就是execl的特点,不会返回,除非启动失败。调用的进程会直接使用当前进程的进程号、父进程号、控制终端、根目录、未处理信号等。
案例2:在代码中使用execlp执行ls命令
#include<stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
printf("执行前:\n");
execlp("ls","ls","-a","-l","-h",NULL);
printf("执行后:\n");
return 0;
}
案例三:在代码中使用execp执行ls命令
#include<stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
printf("执行前:\n");
char *avg[]={"ls","-a","-l","-h",NULL};
execvp("ls",avg);
printf("执行后:\n");
return 0;
}
案例四:vfork和exec配合使用
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include<sys/wait.h>
#include <sys/stat.h>
int main(int argc, char const *argv[])
{
pid_t pid = vfork();
if(pid ==0)
{
execlp("ls","ls","-a","-l","-h",NULL);
}
if(pid>0)
{
int i= 0;
for(i;i<4;i++)
{
printf("i=%d\n",i);
sleep(1);
}
}
return 0;
}
运行:
分析:会先运行子进程,子进程中又使用excelp调用其他程序。