本文主要分享的是关于进程与线程的八股文,既是对自己基础知识的巩固,也是与大家分享。文章中如果观点错误的地方肯定大家予以指正。文章中黑体部分文字为相关问题的要点,各位看官一定要牢牢背下来啊。
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
时间片(timeslice)又称为“量子(quantum)”或“处理器片(processor slice)”是操作系统分配给每个正在运行的进程微观上的一段 CPU 时间。事实上,虽然一台计算机通常可能有多个 CPU,但是同一个 CPU 永远不可能真正地同时运行多个任务。在只考虑一个 CPU 的情况下,这些进程“看起来像”同时运行的,实则是轮番穿插地运行,由于时间片通常很短(在 Linux 上为 5ms-800ms),用户不会感觉到。
时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
使用下图的现实场景来辅助理解可能会更清晰,并发是两个队列交替使用一台咖啡机(如上图)。并行是两个队列同时使用两台咖啡机(如下图)。
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。
在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。
在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。
◼ 新建态:进程刚被创建时的状态,尚未进入就绪队列
◼ 终止态:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然保留在操作系统中等待善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。
◼ 运行态:进程占有处理器正在运行
◼ 就绪态:进程具备运行条件,等待系统分配处理器以便运
行。当进程已分配到除CPU以外的所有必要资源后,只要再
获得CPU,便可立即执行。在一个系统中处于就绪状态的进
程可能有多个,通常将它们排成一个队列,称为就绪队列
◼ 阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程
不具备运行条件,正在等待某个事件的完成
◼ 父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程
(Orphan Process)。
◼ 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init
进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束
了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。
◼ 因此孤儿进程并不会有什么危害
也就是通常说的 Daemon 进程(精灵进程),是Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。
◼ 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。
◼ 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如 SIGINT、SIGQUIT)。
◼ Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等。
执行一个 fork(),之后父进程退出,子进程继续执行。
◼ 子进程调用 setsid() 开启一个新会话。
◼ 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限。
◼ 修改进程的当前工作目录,通常会改为根目录(/)。
◼ 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。
◼ 在关闭了文件描述符0、1、2之后,守护进程通常会打开/dev/null 并使用dup2() 使所有这些描述符指向这个设备。
每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程去释放。
◼ 子进程终止时,若父进程未回收子进程资源,造成子进程残留资源(PCB)存放于内核中,此时子进程就变成了僵尸(Zombie)进程。
◼ 僵尸进程不能被 kill -9 杀死,这样就会导致一个问题,如果父进程不调用 wait() 或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
在子进程销毁时,父进程将受到SIGCHLD信号,父进程应捕捉该信号,并进行处理。调用系统函数wait或waitpid得到子进程的退出状态同时彻底清除掉这个子进程。
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。我们可以通过kill sig pid命令对指定pid的进程发送任意sig信号,在此不罗列所有信号,信号的种类很多大家可以自行百度。对于每个信号在系统中的存在,可以理解为一个数字,系统以二进制进行读取,每一位代表一个信号。我们可以使用系统API设置信号集屏蔽部分信号,或是定义自己的信号处理函数来实现一些功能。如nginx服务器架构中,就使用了信号机制通信,其中的服务器热升级功能就是使用信号机制辅助完成的。
linux提供了2种定时器函数分别为alarm和settimer函数:
①unsigned int alarm(unsigned int seconds):
在seconds秒后出发定时器,本进程将受到SIGALARM信号。
②int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value):
在一定时间后,以周期性的循环对本进程发送SIGALARM信号。
unsigned int alarm(unsigned int seconds);
- 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,
函数会给当前的进程发送一个信号:SIGALARM
- 参数:
seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
取消一个定时器,通过alarm(0)。
- 返回值:
- 之前没有定时器,返回0
- 之前有定时器,返回之前的定时器剩余的时间
- SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
alarm(10); -> 返回0
过了1秒
alarm(5); -> 返回9
alarm(100) -> 该函数是不阻塞的
#include
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
- 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时
- 参数:
- which : 定时器以什么时间计时
ITIMER_REAL: 真实时间,时间到达,发送 SIGALRM 常用
ITIMER_VIRTUAL: 用户时间,时间到达,发送 SIGVTALRM
ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送 SIGPROF
- new_value: 设置定时器的属性
struct itimerval { // 定时器的结构体
struct timeval it_interval; // 每个阶段的时间,间隔时间
struct timeval it_value; // 延迟多长时间执行定时器
};
struct timeval { // 时间的结构体
time_t tv_sec; // 秒数
suseconds_t tv_usec; // 微秒
};
过10秒后,每个2秒定时一次
- old_value :记录上一次的定时的时间参数,一般不使用,指定NULL
- 返回值:
成功 0
失败 -1 并设置错误号
内存映射是一种进程间的通信机制,该机制做了解即可。通过mmap函数可向系统申请一个磁盘文件(或是使用已有文件作为映射)作为映射区,不同的进程间可同时访问该文件实现数据交互的目的。
内存映射是实际存在了一个磁盘文件,系统直接将磁盘文件数据映射到内核缓冲区,这个映射的过程是基于DMA引擎拷贝的,同时用户缓冲区是跟内核缓冲区共享一块映射数据的,建立共享映射之后,就不需要从内核缓冲区拷贝到用户缓冲区了。
使用内存映射实现进程间通信:
1.有关系的进程(父子进程)
- 还没有子进程的时候
- 通过唯一的父进程,先创建内存映射区
- 有了内存映射区以后,创建子进程
- 父子进程共享创建的内存映射区
2.没有关系的进程间通信
- 准备一个大小不是0的磁盘文件
- 进程1 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 进程2 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 使用内存映射区通信
共享内存使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。它和内存映射是完全不同的通信机制,内存映射是实际存在了一个磁盘文件;而共享内存,是实实在在的向操作系统申请一块内存空间,不同进程对该内存进行访问达到数据交互的目的。
1)管道可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
2)它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端
3)它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间),因为匿名管道的创建步骤为在进程fork前创建管道,则在进程fork后,子进程与父进程对于该管道将拥有相同的文件操作符,实现通信目的。
可使用如下api创建匿名管道
// 创建一个管道
//fd[0]为管道的读端用于读数据,fd[1]为管道写端用于写数据
int fd[2];
int ret = pipe(fd);
if(ret == -1) {
perror("pipe");
exit(0);
}
有名管道的操作与匿名管道类似,但是可以在无关的进程之间交换数据;有名管道有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
可使用如下api创建有名管道
创建fifo文件
1.通过命令: mkfifo 名字
2.通过函数:int mkfifo(const char *pathname, mode_t mode);
#include
#include
int mkfifo(const char *pathname, mode_t mode);
参数:
- pathname: 管道名称的路径
- mode: 文件的权限 和 open 的 mode 是一样的
是一个八进制的数
返回值:成功返回0,失败返回-1,并设置错误号