目录
4.进程控制块PCB(Process Contrl Block)
【进程的概述】
1.程序和进程的区别
程序:静态的,占用的是磁盘空间
进程:动态的,进程的(调度、执行、消亡)占用内存空间
进程通俗的来讲,就是一个可执行文件从头执行到尾的过程。
2.单道程序设计和多道程序设计
单道程序设计:类似于队列,先进先出,进程想要被执行调度只有等到前面的进程被执行并且结束。也就是同一时刻只有一个进程可以被执行。这个设计是十分不合理的,所以已经被淘汰了。
多道程序设计:利用时钟中断,在宏观上实现进程间同步运行。但是在微观上其实还是分进程一个个执行。比如三个任务A B C ,利用时钟中断,A执行10us然后记录当前任务位置,退出执行B 10us,在退出执行 C 10us,不断往复循环,就可以在肉眼上实现多个任务共同执行。
3.并行和并发
并行和并发两者都是多任务同时执行,但是从微观的角度看,并行是同时执行,而并发其实是利用中断来实现近似于同时执行的情况。
并行:同一时刻有多个处理器同时处理多个任务
微观上的多指令同时执行,多个任务在多个处理器上同时执行
多个CPU,多核处理器,可以同时处理多个任务
如下图。A B C 三个任务在微观的角度看,也是同时执行的。利用多核处理器来同时处理多个任务。
并发:同一时刻只有一个任务执行,利用时钟中断从宏观上实现多任务同步进行
多个指令同时执行,但是每个时间段只有一个指令能够执行,宏观上的多个指令同时执行
只有一个CPU,单核处理器,所以不能像并行那样多核处理指令
下图是以微观的角度来看并发,同一时刻只有一个任务可以执行,但是每个任务执行的时间是非常短的,所以从宏观看,近似于多任务同时执行。
4.进程控制块PCB(Process Contrl Block)
当进程在运行的时候,系统会为每一个进程分配一个进程控制块,用来维护进程相关信息。PCB存在于系统的内核空间中
Linux内核的进程控制块是task_struct结构体,其内部比较重要的成员如下:
其中pid为进程的进程号,是一个很重要的数据,每一个进程都有一个独一无二的进程号,类型为pid_t(非负整数),在Linux中通过 ps 指令可以查看进程号。
pid:进程id,系统中每个进程有唯一的id,使用 pid_t 类型表示。注意每个线程的pid都不相同。
tgid:thread group id,linux引入线程组的概念。一个线程组所有线程与领头线程具有相同的pid,存入tgid字段,getpid()返回当前进程的 tgid 值而不是 pid 的值。
进程的状态:
就绪(初始+就绪):等CPU分配时间片
运行:占用CPU
挂起:等待除CPU以外的其它资源,主支放弃CPU
停止:程序运行停止
进程切换时需要保存和恢复的一些CPU寄存器。
描述虚拟地址空间的信息。
描述控制终端的信息。
当前工作目录。
umask掩码。
文件描述符表,包含很多指向file结构体的指针。
和信号相关的信息。
用户id和组id。
会话(Session)和进程组:
进程组:一组相关进程的集合;
会话:一组相关进程组的集合;
进程可以使用的资源上限(Resource Limit)。
5.进程的状态
进程分为三个状态:就绪态、等待态、执行态
就绪态:执行条件全部满足,等待CPU调度
等待态:执行条件没有全部满足,等待条件满足
执行态:正在被CPU执行调度
5.1进程状态的查看
ps aux 相当于Window下的任务管理器
ps aux | grep 进程名 通过管道读取感兴趣进程号
ps的额外可选参数
配合ps使用,比如上面的aux就是-a -u -x 的组合
【进程号PID】
每个进程都由一个进程号来标识,其类型为pid_t(整型),进程号的范围:0~32767。进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用
系统中有三种进程号
进程号 PID 标识一个进程的非负整数
父进程号PPID 父进程号
进程组号PGID 多个进程的集合
包含关系如下:
1.进程号的获取
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
功能
获得调用此函数进程的进程号
参数
无
返回值
当前进程号
1.1获取当前进程的进程号
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
pid_t pid = getpid();
printf("当前进程号为%d \n",pid);
while(1)
;
return 0;
}
2.父进程号的获取
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
功能
获得调用此函数的进程的父进程号
参数
无
返回值
调用此函数进程的父进程号
3.进程组号的获取
#include <sys/types.h>
#include <unistd.h>
pid_t getpgid(pid_t pid);
功能
获取进程组号
参数
pid:进程号
返回值
参数为0 ,返回当前进程的进程组号,否则返回指定进程的进程组号
如果一个进程组号和进程中一个进程的进程号相同,那么该进程就是组长进程。
【创建进程fork】
系统允许一个进程创建新的进程,那么新创建的进程称为子进程,当前进程为父进程。同时子进程还可以在创建子进程。
1.进程创建函数fork
#include <unistd.h>
#include <sys/types.h>
pid_t fork(void);
功能
用于从当前已经存在的进程中创建一个进程。当前进程称为父进程,新的进程称为子进程。
参数
无
返回值
成功:子进程中返回0,父进程中返回子进程的进程号,类型为pid_t
失败:返回-1。失败的原因可能是进程数已经达到上限,或者系统内存不足
2.fork创建的父子进程之间的关系
使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。地址空间:包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。子进程所独有的只有它的进程号,计时器等。因此,使用fork函数的代价是很大的。
当子进程继承过父进程的所有代码后,从fork后面开始执行。
2.1 fork拥有两个返回值的意义
fork函数的调用,会使父进程除了进程号以及计时器外几乎所有东西都会复制给子进程,同时,两个进程是近乎同时执行的,不能区分出来是谁先谁后(fork一般是父进程先执行)。那么系统到底是要怎么区分哪个进程是父进程,那个进程是子进程呢?只能通过fork函数的返回值来判断(父子进程是接受fork返回值后在往下运行)。如果fork返回值为0,那么当前进程为子进程,如果是大于0的数,那么就是接受到的子进程号,当前进程就是父进程。
2.2 子进程的创建
#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("创建失败\n");
}
else if( 0 == pid )
{
printf("当前为子进程,进程号为%d\n",getpid());
}
else if( pid > 0 )
{
printf("当前为父进程,进程号为%d\n",getpid());
}
while(1)
;
return 0;
}
父子进程空间是互相独立的,两个是完全不同的空间。
当父进程创建了一个子进程,那么这两个是完全独立的空间,两个是不会互相影响的。
父子进程是近乎同时执行的,相当于多个任务同时运行。
一般用于多个任务需要同时执行的情况,使用多进程达到多任务同时运行的目的
如下图所示,为了证明多个进程之间是同时执行的,在父子进程之间都设置了一个死循环,如果进程与进程之间不是互相独立同时运行的,那么肯定会卡死在一个死循环里面,但是可见下图并没有卡死在哪个单独的进程里,可见,进程是同步运行的。
【特殊进程】
分别有三种特殊的进程:孤儿进程、僵尸进程、守护进程
1.孤儿进程
孤儿进程就是,父进程先与子进程结束,导致子进程会被一号进程给接管,让一号进程变为该子进程的父进程。孤儿进程是没有危害的。
如下图所示
原本子进程的进程号为10681,父进程先结束后,父进程进程号变为1
2.僵尸进程
僵尸进程其实就是父进程在子进程结束后,没有回收子进程的资源所导致的。也就是进程结束了,但是还占用内存。
3.守护进程
守护进程其实就是一种脱离终端,在后台运行的进程,也是一种特殊的孤儿进程,用于执行系统的特殊任务。
【父进程回收子进程资源】
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)
父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程
一个wait或waitpid只能清理一个子进程,清理多个用循环
1.wait函数(一般用于单个子进程结束回收资源)
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
功能
等待任意子进程结束,并且回收子进程的资源(带有阻塞特性)
参数
status:进程退出时候的状态
返回
成功:已经结束的子进程号
失败:-1
需要注意的是
wait函数是带阻塞特性的,如果父进程没有子进程,那么会立即返回,否则会阻塞,直到有子进程退出才会解除阻塞状态。
如果需要判断退出的状态值,可以把status传入宏中来判断。
WIFEXITED(status)如果子进程是正常终止的,取出的字段值非零。
WEXITSTATUS(status)返回子进程的退出状态,退出状态保存在status变量的8~16位
2.waitpid函数(一般用于多个子进程结束回收资源)
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
比较重要的是该函数的形参选择:
pid:参数形参有一下几种情况
pid > 0 :等待进程号为pid的进程结束
pid = 0 :等待同一进程组的任意子进程结束,如果子进程已经加入其他进程组,则不等待它。
pid = -1 :等待任意子进程结束,效果和wait一样(常用)
pid < -1 :等待进程组的任意进程结束,pid的绝对值对进程组号
status参数通过指针传入来接受状态值
option:
0 :同wait,阻塞父进程
WNOHANG :如果没有任何结束的子进程,则立刻返回(常用)
WUNTRACED :如果子进程被暂停了则立即返回,并且不理会子进程结束状态
返回值
0 没有任何子进程结束
>0 检测到子进程结束,返回进程号
<0 全部子进程结束
一般waitpid是在父进程中使用,用来回收多个子进程的资源。一般是通过父进程不断while1死循环扫描该函数的返回值来判断子进程
【多进程的创建】
1.for循环创建多进程的问题
一般情况下,当我们想创建几个进程,仅需写一个for循环来创建。但是这样可能会遇到一些问题。
如下这个代码,本意是创建两个子进程,但是实际上包括父进程总共会有四个进程
for(i = 0; i < 2; i++)
{
pid_t pid = fork();
}
一开始 i == 0,父进程创建了一个子进程1,子进程会拥有父进程除了进程号、计时器等几乎一切东西。当 i == 1 ,父进程再次创建一个子进程2,而第一个创建的子进程1也会创建子进程,所以当 i==1的时候,父进程创建了一个子进程2,子进程1创建了一个孙进程1.当i == 2 ,退出循环,此时拥有四个进程。
这并不是我们的本意,因为如果不加以显示,会产生非常多不需要用到的进程。是以平方来计算的,非常的浪费内存资源。假如本意是创建五个子进程,那么会创建2的5次方-1个子进程。
2.防止父进程创建的子进程创建子进程
我们都知道,fork函数拥有两个返回值,就是为了用来区分到底那个是子进程那个是父进程,在这里,它就派上了用场,如果区分出哪个是子进程,不让它继续创建子进程即可。
当判断出该进程是子进程后,直接退出for循环,防止fork函数再次被调用。
for(i = 0; i < 2; i++)
{
pid_t pid = fork();
if( 0 == pid )
break;
}
2.1通过限制创建子进程
通过返回值pid的值来判断,如果为0,则是子进程,可分配任务,如果大于0则为父进程,方便资源回收。
2.2给不同的子进程分配不同的任务
虽然已经是解决了子进程会再次创建子进程的问题,但是当进程很多的时候,该如何判断哪个进程该去完成什么任务呢。
我们在创建多进程的时候,是使用for循环来判断的。里面的 i 就是进程分配任务的依据。
创建多任务并分配不同任务
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//创建的进程数目
#define N 3
int main(int argc, char const *argv[])
{
//创建进程
int i = 0;
for ( i = 0; i < N; i++)
{
pid_t pid = fork();
//printf("当前进程号为 %d ,父进程为 %d \n",getpid(),getppid());
//防止子进程继续创建子进程
if( 0 == pid )
break;
}
if( 0 == i )//进程1
{
int i = 0;
for ( i = 3; i > 0; i--)
{
printf("子进程%ds后结束,进程号为%d\n",i,getpid());
sleep(1);
}
_exit(-1);
}
else if( 1 == i )//进程2
{
int i = 0;
for ( i = 6; i > 0; i--)
{
printf("子进程%ds后结束,进程号为%d\n",i,getpid());
sleep(1);
}
_exit(-1);
}
else if( 2 == i )//进程3
{
int i = 0;
for ( i = 7; i > 0; i--)
{
printf("子进程%ds后结束,进程号为%d\n",i,getpid());
sleep(1);
}
_exit(-1);
}
else if( N == i )//父进程,用于回收子进程资源
{
while(1)
{
pid_t pid = waitpid(-1,NULL,WNOHANG);
if( pid > 0 ) //成功返回
{
printf("进程%d退出\n",pid);
}
else if( pid < 0 ) //所有子进程退出
{
printf("所有子进程结束\n");
break;
}
}
}
return 0;
}
【进程的补充】
1.终端
Linux的终端就是控制台,是用户与内核交互的平台,通过输入指令来控制内核完成操作。外形是一个方框,有一个光标在闪烁。
用户通过输入shell命令,通过解析器来完成对内核的访问。
当在系统中执行可执行文件的时候,终端的控制权限就不是交给bash进程了,而是交给了打开的可执行文件。比如说在当前终端下输入 ./a.out 就是打开这个可执行文件,此时终端的控制权限由bash进程转交给了a.out,则此时无法通过输入命令来控制,因为终端控制权不在bash解析器上。
当子进程被杀死的时候,终端的控制权重新回到bash进程上。
如果进程创建了子进程,那么当父进程结束,终端的控制权重新回到bash进程后,子进程也仅仅只遗留了输出的权限。
2.进程组
进程组就是多个进程的集合,当父进程创建子进程的时候,默认是同一个进程组的。
如何杀死一个进程组所有的进程
使用如下命令:可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀死
进程组ID之所以是负数,是为了避免和进程号冲突。(一般操控进程组都是用负数)
组长进程就算被杀死,进程组也还是存在的,只是下一个进程来代替成为组长进程。
2.1操控进程组的函数
#include <unistd.h>
pid_t getpgrp(void);
功能
获得当前进程的进程组号
参数
无
返回值
当前进程的进程组号
#include <unistd.h>
pid_t getpgid(pid_t pid);
功能
返回指定进程的进程组号
形参
进程号,如果为0就是返回当前进程的进程组号
返回值
进程组号,失败返回-1
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
功能
改变进程的进程组,可以用来加入进程或者创建进程组
参数
pid 进程号
pgid 加入或者创建的进程组号
返回值
成功 0
失败 -1
3.会话
会话(session)是若干进程组的集合,系统中的每一个进程也必须从属于某一个会话。
一个会话最多只有一个控制终端(也可以没有),该终端为会话中所有进程组中的进程所共有。
一个会话中只会有一个前台进程组,只有前台进程组中的进程才可以和控制终端进行交互
在拥有控制终端的会话中,session leader 也被称为控制进程(controlling process),一般来说控制进程也就是登入系统的 shell 进程(login shell)
如果有一个新的会话被创建,那么该会话会脱离当前终端(相当于在后台执行)
3.1会话的创建
会话的创建步骤
如下函数用来设置进程为会话
#include <unistd.h>
pid_t setsid(void);
功能
创建一个会话,并以自己的进程号设置进程组ID,同时也是新会话的ID。调用了该函数的进程,即是组长进程,有是会话首进程。
参数
无
返回值
成功:返回调用进程的会话id
失败:-1
可见,想要让一个进程成为会话,首先创建此进程的父进程不能是组长进程。然后要让此进程成为新的进程组组长。同时,结束父进程,让该进程成为孤儿进程,在调用setsid函数,成为会话,该进程不仅是进程组组成,还是创建的新的会话的会话首进程。
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//创建父进程,并且结束父进程让子进程称为会话
pid_t pid = fork();
if( pid > 0 )
_exit(-1);
//子进程
printf("子进程的pid为%d\n",getpid());
//创建子进程为会话
setsid();
while(1)
;
return 0;
}
3.2守护进程的创建
其实1 2 都是创建会话的步骤,后面是按照此步骤对会话的修改,使这个会话成为守护进程。
3 4 5 操作如下
【vfork创建子进程】
vfork 和 fork 都是用来在当前进程中创建子进程的,但是两者还是有区别的。
vfork 创建的子进程和父进程是共享空间的,不是独立的,并且子进程一定会先执行
fork 创建的子进程和父进程是相互独立的,并且谁先执行是不确定的(一般父进程先)
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
功能
fork 和 vfork 都是创建一个子进程,但是他们两个子进程是有区别的
返回值
成功:子进程返回0,父进程返回子进程进程号
失败:-1
vfork创建的子进程一定会先于父进程执行,因为父子进程是公用一块空间的。除非子进程exit退出或者使用exec在进程中启动另一个进程(前者在父进程看来是退出了,后者是相当于子进程中启动另一个进程,相当于独立出去),此时父进程才会执行。
证明父子进程是同一块空间
【exec函数族】
族,就是众多函数的统称
exec函数族的目的,就是在进程中打开另一个进程
函数中有l(list)使用列表方式传参
函数中有v(vector),使用指针数组方式传参
p表明到系统环境中找可执行文件
e表示exec可以使用环境变变量值
execl打开ls
exec会直接接管当前进程,除了原本进程的进程号之外,其他所有东西均会被新的进程接替,只剩下一个原本的进程号
一个进程调用exec后,除了进程ID,进程还保留了下列特征不变:父进程号进程组号控制终端根目录当前工作目录 进程信号屏蔽集 未处理信号...
如下图可知,使用execl在进程中打开了新的进程之后,原本的进程就不会在执行了(原本的进程就已经被替换了,类似夺舍)。
vfork和exec配合使用
(fork和exec也可以配合使用,在fork创建子进程后写一句立刻接上exec,子进程会被exec函数启动的进程取代,比如说可以启动shell脚本什么的)
vfork创建的进程能够保证子进程是先执行的,那么配合exec,让子进程独立出来,此时可以执行父进程。(可以让子进程先执行)
#include <stdio.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//创建进程
pid_t pid = vfork();
if( pid > 0 ) //父进程
{
sleep(3);
printf("父进程执行\n");
}
else if( 0 == pid ) //子进程
{
sleep(3);
printf("子进程执行\n");
execl("/bin/ls","ls","-l",NULL);
}
return 0;
}