程序:静态的 占磁盘空间
进程:可执行文件 执行的动态过程 动态(创建、进程的调度、消亡) 占内存。
不管是并发、并行 都是多个任务 同时执行。
指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行
并行:多核 多任务真正的并行
并发:单核 多任务 通过时间片 轮转 是宏观上的并行。
进程运行时,内核为进程每个进程分配一个PCB(进程控制块),维护进程相关的信息,Linux内核的进程控制块是task_struct结构体:
部分截图:
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。 在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。 在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞。
0号进程 是内核进程 也是调度进程---->1初始化进程—>直接或者间接创建了所有进程
如果想查看进程关系:pstree
pstree -p 会显示进程号
每个进程都由一个进程号来标识,其类型为 pid_t(整型),进程号的范围:0~32767。进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用。
标识进程的一个非负整型数。
任何进程( 除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。如,A 进程创建了 B 进程,A 的进程号就是 B 进程的父进程号
进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)
getpid函数
#include
#include
pid_t getpid(void);
功能:
获取本进程号(PID)
参数:
无
返回值:
本进程号
getppid函数
#include
#include
pid_t getppid(void);
功能:
获取调用此函数的进程的父进程号(PPID)
参数:
无
返回值:
调用此函数的进程的父进程号(PPID)
getpgid函数
#include
#include
pid_t getpgid(pid_t pid);
功能:
获取进程组号(PGID)
参数:
pid:进程号
返回值:
参数为 0 时返回当前进程组号,否则返回参数指定的进程的进程组号
#include
#include
#include
int main(int argc,char *argv[])
{
printf("进程号的PID:%d\n", getpid());
printf("父进程的PPID:%d\n", getppid());
printf("进程组号的PGID:%d\n", getpgid(0));
while(1);//阻塞 方便终端 验证
return 0;
}
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型.
#include
#include
pid_t fork(void);
功能:
用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
参数:
无
返回值:
成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为整型。
失败:返回-1。
失败的两个主要原因是:
1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。
2)系统内存不足,这时 errno 的值被设置为 ENOMEM
#include
#include
#include
int main(int argc,char *argv[])
{
//创建一个子进程
pid_t pid = fork();
if(pid < 0)//创建失败返回的是-1
{
perror("fork");
return 0;
}
else if(pid == 0)//单纯的认为 就是子进程
{
//任务一
while(1)
{
static int i=0;
printf("i = %d\n",i++);
sleep(1);
}
}
else if(pid > 0)//单纯的认为 就是父进程
{
//任务二
while(1)
{
static int j=0;
printf("j = %d\n",j++);
sleep(1);
}
}
return 0;
}
运行结果:
#include
#include
#include
int main(int argc,char *argv[])
{
int num = 10;
//创建一个子进程
pid_t pid = fork();
if(pid < 0)//创建失败返回的是-1
{
perror("fork");
return 0;
}
else if(pid == 0)//单纯的认为 就是子进程
{
num =1000;
printf("子进程%d 中的num=%d\n", getpid(),num);
getchar();//阻塞一下
}
else if(pid > 0)//单纯的认为 就是父进程
{
sleep(5);//等5秒 保证子进程 给num复制
printf("父进程%d 中的num=%d\n", getpid(),num);
getchar();//阻塞一下
}
return 0;
}
使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。 地址空间: 包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。 子进程所独有的只有它的进程号,计时器等。因此,使用fork函数的代价是很大的
在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。 父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
1、wait() 和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。
2、一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环
函数说明:
#include
#include
pid_t wait(int *status);
功能:
等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
参数:
status : 进程退出时的状态信息。
返回值:
成功:已经结束子进程的进程号
失败: -1
调用 wait() 函数的进程会挂起(阻塞),直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒(相当于继续往下执行)。
若调用进程没有子进程,该函数立即返回;
若它的子进程已经结束,该函数同样会立即返回,并且会回收那个早已结束进程的资源
总结一句话:wait 有子进程运行 我就等待
如果参数 status 的值不是 NULL,wait() 就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的。 这个退出信息在一个 int 中包含了多个字段,直接使用这个值是没有意义的,我们需要用宏定义取出其中的每个字段。
取出子进程的退出信息 WIFEXITED(status) 如果子进程是正常终止的,取出的字段值非零。 WEXITSTATUS(status) 返回子进程的退出状态,退出状态保存在status变量的8~16位。在用此宏前应先用宏WIFEXITED判断子进程是否正常退出,正常退出才可以使用此宏。 注意:此status是个wait的参数指向的整型变量
#include
#include
#include
#include
#include
int main(int argc,char *argv[])
{
pid_t pid = fork();//创建一个子进程
if(pid < 0)
{
perror("fork");
return 0;
}
else if(pid == 0)//子进程
{
for(int i=0;i<5;i++)
{
printf("子进程:%d的i=%d\n",getpid(),i);
sleep(1);
}
//退出exit
exit(10);
}
else if(pid > 0)//父进程
{
//父进程回收子进程的资源
printf("父进程%d正在等子进程%d\n",getpid(), pid);
int status = 0;
pid_t son_pid = wait(&status);//阻塞
printf("子进程%d退出了\n",son_pid );
if( WIFEXITED(status))//正常退出
{
//获取状态码
printf("退出的状态值为:%d\n",WEXITSTATUS(status));
}
}
return 0;
}
#include
#include
pid_t waitpid(pid_t pid, int *status, int options);
功能:
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
参数:
pid : 参数 pid 的值有以下几种类型:
pid > 0 等待进程 ID 等于 pid 的子进程。
pid = 0 等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。
pid = -1 等待任一子进程,此时 waitpid 和 wait 作用一样。
pid < -1 等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。
status : 进程退出时的状态信息。和 wait() 用法一样。进程退出时的状态信息。
options : options 提供了一些额外的选项来控制 waitpid()。
0:同 wait(),阻塞父进程,等待子进程退出。
WNOHANG:没有任何已经结束的子进程,则立即返回。
WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(由于涉及到一些跟踪调试方面的知识,加之极少用到)
返回值:
waitpid() 的返回值比 wait() 稍微复杂一些,一共有 3 种情况:
1) 当正常返回的时候,waitpid() 返回收集到的已经回收子进程的进程号;
2) 如果设置了选项 WNOHANG,而调用中 waitpid() 还有子进程在运行,且没有子进程退出,返回0; 父进程的所有子进程都已经退出了 返回-1; 返回>0表示等到一个子进程退出
3) 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在,如:当 pid 所对应的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid() 就会出错返回,这时 errno 被设置为 ECHILD
#include
#include
#include
#include
#include
int main()
{
//创建一个进程
pid_t pid = fork();
if(pid == 0)//表示子进程
{
int i=0;
for(i=5;i>0; i--)
{
printf("子进程%d生命值%d\n",getpid(), i);
sleep(1);
}
exit(0);
}
else if(pid > 0)//父进程
{
//pid是子进程的id
int my_status=0;
printf("父进程等待子进程%d中......\n",pid);
waitpid(pid, &my_status, 0);
printf("子进程%d已退出\n",pid);
if(WIFEXITED(my_status))
{
printf("退出的状态值:%d\n", WEXITSTATUS(my_status) );
}
}
return 0;
}
需求:我想创建2个子进程
#include
#include
#include
#include
#include
int main()
{
int i=0;
for(i=0;i<2;i++)
{
pid_t pid = fork();//创建了3个子进程
}
while(1);
return 0;
}
运行分析:
分析:循环两次 得到3个子进程
如果能保证 子进程 不会创建新的子进程 那么 对父进程 创建的子进程个数 就可以得到有效控制
如果我们创建n个子进程 那么i=0,1,2,3,…,n-1都是子进程 i==n表示是父进程
#include
#include
#include
#include
#include
int main()
{
int i=0;
for(i=0;i<2;i++)
{
pid_t pid = fork();
//子进程 不创建 新的子进程(子进程 立马 退出 for循环就行)
if(pid == 0)
break;
}
//区分父子进程
if(i==0)//子进程1
{
printf("子进程1 进程号为%d\n", getpid());
getchar();
//具体的任务一代码
}
else if(i==1)//子进程2
{
printf("子进程2 进程号为%d\n", getpid());
getchar();
//具体的任务二代码
}
else if(i==2)//父进程 回收子进程的资源
{
printf("父进程 进程号为%d\n", getpid());
while(1)
{
//参数-1 所有子进程 NULL不关心退出的状态值 WNOHANG不阻塞
pid_t pid = waitpid(-1, NULL, WNOHANG);
if(pid > 0)//检测到子进程退出
{
printf("子进程%d退出了\n",pid);
}
else if(pid == 0)//还有子进程
{
continue;//继续waitpid等待
}
else if(pid == -1)//所有子进程都退出了
{
break;
}
}
}
return 0;
}
子进程终止,父进程尚未回收子进程资源,子进程残留资源(PCB)存放于内核中,子进程变成僵尸(Zombie)进程。 这样就会导致一个问题,如果父进程不调用wait() 或 waitpid() 的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
#include
#include
#include
#include
#include
int main()
{
int i=0;
for(i=0;i<2;i++)
{
pid_t pid = fork();
if(pid == 0)
break;
}
//区分父子进程
if(i==0)//子进程1
{
printf("子进程1 进程号为%d\n", getpid());
}
else if(i==1)//子进程2
{
printf("子进程2 进程号为%d\n", getpid());
}
else if(i==2)//父进程 回收子进程的资源
{
while(1);//父进程不结束
}
return 0;
}
父进程运行结束,但子进程还在运行(未运行结束)的子进程就称为孤儿进程(Orphan Process)。 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。 因此孤儿进程并不会有什么危害.
#include
#include
#include
#include
#include
int main()
{
int i=0;
for(i=0;i<2;i++)
{
pid_t pid = fork();
if(pid == 0)
break;
}
//区分父子进程
if(i==0)//子进程1
{
printf("子进程1 进程号为%d\n", getpid());
while(1);
}
else if(i==1)//子进程2
{
printf("子进程2 进程号为%d\n", getpid());
while(1);
}
else if(i==2)//父进程 回收子进程的资源
{
printf("父进程 进程号为%d\n", getpid());
}
return 0;
}
在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端(Controlling Terminal),进程中,控制终端是保存在PCB中的信息,而fork会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。 默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上
获取终端的名称:
#include
char *ttyname(int fd);
功能:由文件描述符查出对应的文件名
参数:
fd:文件描述符
返回值:
成功:终端名
失败:NULL
下面我们借助ttyname函数,通过实验看一下各种不同的终端所对应的设备文件
#include
#include
#include
#include
#include
int main()
{
pid_t pid = fork();
if(pid == 0)//子
{
printf("子进程标准输入的设备名:%s\n",ttyname(0));
printf("子进程标准输出的设备名:%s\n",ttyname(1));
printf("子进程标准错误输出的设备名:%s\n",ttyname(2));
}
else if(pid > 0)//父
{
sleep(1);
printf("父进程标准输入的设备名:%s\n",ttyname(0));
printf("父进程标准输出的设备名:%s\n",ttyname(1));
printf("父进程标准错误输出的设备名:%s\n",ttyname(2));
}
return 0;
}
进程组 进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。 每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。 当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID为第一个进程ID(组长进程)。所以,组长进程标识:其进程组ID为其进程ID
可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死
总结:一个或多个进程的集合 就是进程组。每个进程组 有自己的组ID 这个组ID就是进程组号。
终端运行的进程 如果没有加入任何一个组 那么该进程 自成一个进程组 (进程组ID == 当前进程的ID)
组长进程:如果进程的ID == 进程组的ID 那么这个进程就叫组长进程。
#include
#include
#include
#include
#include
#include
int main()
{
printf("进程%d\n",getpid());
pid_t pid = fork();
if(pid == 0)
{
printf("子进程ID:%d\n",getpid());
pid_t pid = fork();
if(pid == 0)
{
printf("孙进程ID:%d\n",getpid());
getchar();
}
getchar();
}
else if(pid > 0)
{
printf("父进程ID:%d\n",getpid());
getchar();
}
return 0;
}
运行结果:
原理图:
kill -9 -进程组ID:进程组中的所有进程都会结束。
进程组啥时候解散?进程组中的所有进程都结束的时候才解散。
进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。 一个进程可以为自己或子进程设置进程组ID。
#include
pid_t getpgrp(void); /* POSIX.1 version */
功能:获取当前进程的进程组ID
参数:无
返回值:总是返回调用者的进程组ID
pid_t getpgid(pid_t pid);
功能:获取指定进程的进程组ID
参数:
pid:进程号,如果pid = 0,那么该函数作用和getpgrp一样
返回值:
成功:进程组ID
失败:-1
int setpgid(pid_t pid, pid_t pgid)
功能:
改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新进程组。
参数:
将参1对应的进程,加入参2对应的进程组中
返回值:
成功:0
失败:-1
会话是一个或多个进程组的集合。
会话是一个或多个进程组的集合。 一个会话可以有一个控制终端。这通常是终端设备或伪终端设备; 建立与控制终端连接的会话首进程被称为控制进程; 一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组; 如果一个会话有一个控制终端,则它有一个前台进程组,其它进程组为后台进程组; 如果终端接口检测到断开连接,则将挂断信号发送至控制进程(会话首进程)
会话首进程特点:进程ID == 进程组ID == 会话ID
前台进程组:拥有当前控制终端的进程组
后台进程组:不拥有当前控制终端的进程组
1) 调用进程不能是进程组组长,该进程变成新会话首进程(session header)
2) 该调用进程是组长进程,则出错返回
3) 该进程成为一个新进程组的组长进程
4) 需有root权限(ubuntu不需要)
5) 新会话丢弃原有的控制终端,该会话没有控制终端。
6) 建立新会话时,先调用fork, 父进程终止,子进程调用setsid
API函数介绍 getsid函数:
#include
pid_t getsid(pid_t pid);
功能:获取进程所属的会话ID
参数:
pid:进程号,pid为0表示查看当前进程session ID
返回值:
成功:返回调用进程的会话ID
失败:-1
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
setsid函数:
setsid函数:
#include
pid_t setsid(void);
功能:
创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID。调用了setsid函数的进程,既是新的会长,也是新的组长。
参数:无
返回值:
成功:返回调用进程的会话ID
失败:-1
#include
#include
#include
#include
#include
#include
int main()
{
printf("进程%d\n",getpid());
//创建会话
pid_t pid = fork();
if(pid > 0)//父进程
exit(0);
//子进程设置会话
setsid();
while(1);
return 0;
}
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。 守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。 Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等。
1) 创建子进程,父进程退出(必须) 所有工作在子进程中进行形式上脱离了控制终端
2) 在子进程中创建新会话(必须) setsid()函数 使子进程完全独立出来,脱离控制
3) 改变当前目录为根目录(不是必须) chdir()函数 防止占用可卸载的文件系统 也可以换成其它路径
4) 重设文件权限掩码(不是必须) umask()函数 防止继承的文件创建屏蔽字拒绝某些权限 增加守护进程灵活性
5) 关闭文件描述符(不是必须) 继承的打开文件不会用到,浪费系统资源,无法卸载
6) 开始执行守护进程核心工作(必须) 守护进程退出处理程序模型
#include
#include
#include
#include
#include
int main()
{
//1、创建会话
pid_t pid = fork();
if(pid > 0)//父进程
exit(0);
//子进程设置会话
setsid();
//2、更改目录
chdir("/");
//3、设置掩码
umask(0002);
//4、关闭文件描述符0 1 2
close(0);
close(1);
close(2);
while(1)
{
;//具体守护进程的服务程序
}
return 0;
}
vfork函数:创建一个新进程
pid_t vfork(void)
功能:
vfork函数和fork函数一样都是在已有的进程中创建一个新的进程,但它们创建的子进程是有区别的。
返回值:
创建子进程成功,则在子进程中返回0,父进程中返回子进程ID。出错则返回-1。
vfork保证子进程先运行,在子进程调用exec或exit之后,父进程才可能被调度运行。 vfork和fork一样都创建一个子进程,但vfork并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不访问该地址空间。相反,在子进程中调用exec或exit之前,它在父进程的地址空间中运行,在exec之后子进程会有自己的进程空间.
#include
#include
#include
#include
#include
int main()
{
pid_t pid = vfork();
if(pid == 0)//子进程
{
int i=0;
for(i=5;i>0;i--)
{
printf("子进程%d i=%d\n",getpid(),i);
sleep(1);
}
exit(0);
}
else if(pid>0)//父进程
{
printf("父进程%d\n",getpid());
}
return 0;
}
vfork创建的子进程 会先执行
如果父进程想执行:
子进程调用exit、_exit、退出,父进程才会执行。
子进程调用exec函数族(系统会为调用exec的进程 重新分配资源) 父进程也会运行。
#include
#include
#include
#include
#include
int main()
{
pid_t pid = vfork();
if(pid == 0)//子进程
{
printf("子进程2秒后启动exec\n");
sleep(2);
printf("子进程已启动exec\n");
execlp("ls","ls",NULL);
sleep(10);
}
else if(pid>0)//父进程
{
printf("父进程%d\n",getpid());
}
return 0;
}
在进程内部 启动外部可执行程序。
exec 指的是一组函数,一共有 6 个:
l(list): 参数地址列表,以空指针结尾。 参数地址列表 char *arg0, char *arg1, …, char *argn, NULL v(vector)
p(path) 按PATH环境变量指定的目录搜索可执行文件
e(environment): 存有环境变量字符串地址的指针数组的地址。
v(vector): 存有各参数地址的指针数组的地址
#include
#include
#include
#include
#include
int main()
{
#if 0
//execl
//int execl(const char *path, const char *arg, .../* (char *) NULL */);
printf("execl前\n");
execl("/bin/ls", "ls", "-a","-l","-h",NULL);
printf("execl后\n");//不会执行
#endif
#if 0
//execlp
//int execlp(const char *file,cconst char *arg, ... /* (char *) NULL */);
printf("execlp前\n");
execlp("ls", "ls", "-a","-l","-h",NULL);
printf("execlp后\n");//不会执行
#endif
#if 0
//execv
//int execv(const char *path, char *const argv[]);
char *argv[]={"ls", "-a","-l","-h",NULL};
printf("execv前\n");
execv("/bin/ls", argv);
printf("execv后\n");//不会执行
#endif
#if 1
//execvp
//int execvp(const char *file, char *const argv[]);
char *argv[]={"ls", "-a","-l","-h",NULL};
printf("execvp前\n");
execvp("ls", argv);
printf("execvp后\n");//不会执行
#endif
return 0;
}