【彻底看懂操作系统】【进程与线程的介绍、资源共享、进程/线程间通信】

计算机通常需要并发的执行多个程序,为了对这些并发执行的程序进行控制和隔离,把一个个运行中的程序叫做进程。不仅能用进程描述和管理程序执行的过程,也成为了资源共享的基本单位。然而进程开销太大,又引入了线程的概念。

1.程序的并发执行与进程抽象解决的问题

通常情况下,一台计算机同时运行多个程序,而在同一时刻下每个CPU只能运行一个程序,一个计算机中的需要同时运行的程序数量可能远多于CPU数量。所以,要解决的问题就成了:如何在有限CPU数量下提供多个程序对CPU 的复用,为用户制造多个程序运行的假象。因此操作系统对CPU进行了细粒度的时间片划分以完成进程的时域共享以实现进程的并发执行。让多个程序交替地、陆续地占用CPU,只要CPU计算速度和进程切换的速度足够快,对于用户而言,这些交替着执行的程序就是在同时执行。
要实现并发执行就得处理以下问题:

  1. 程序执行到某处的状态的保存和恢复。当这个进程时间片结束后需要切换到另一个进程,保存了当前执行的状态和进度(如局部变量result值、PC指针等),就能在下一个本进程时间片到来之后接着往下运行,因此操作系统就要为程序提供状态的保存和恢复。
  2. 资源共享制约。并发执行中的程序需要共享计算机系统中的CPU、内存、打印等资源,任意一个程序对这些资源的状态的改变都会影响其他程序的运行环境,造成程序之间互相制约。因此操作系统也需要为程序提供资源的隔离。保证并发的程序能够执行下去。

2.进程的动态特征刻画

为了反映这些进程的执行进度和状态,操作系统采用进程控制块PCB来描述和管理进程。

2.1 一个进程控制块PCB里面有什么信息支持它对进程进行描述和管理:

创建一个进程时首先为这个进程创建这个进程的PCB,操作系统就知道有这么一个进程,当进程完成后释放掉PCB,进程随之终止,操作系统也就不知道还有这个进程了。

2.1.1 用来描述进程的信息
  1. 让操作系统区分不同进程的进程号(进程标识符)。
  2. 一个计算机可以有多个用户使用,在开机登录时切换用户就就是我们常见的了,每个程序是有自己的用户在使用的,那么也就有用户标识符了。
  3. 家族关系,就是我们的进程的父进程是谁,什么是父进程——现在我们打开一个浏览器,在浏览器上打开了十几个网页,关闭一个网页,其它网页不会关闭,而当你点击浏览器右上角的叉号时,所有网页都关闭了。这个例子中,浏览器就是打开的网页的父进程。除了计算机中特殊的0号进程(在操作系统启动时,0号进程进行内核初始化的工作,由他创建1号进程,1号进程完成用户空间初始化,成为init进程,是之后创建的所有进程的父进程),所有的进程都有可能有子进程或父进程。Linux进程源文件中家族关系的结构体描述如下:
//源文件:include/linux/sched.h
	struct task_struct __rcu *real_parent;//指向真正父进程
	struct task_struct __rcu *parent;//指向跟踪当前进程的进程
	struct list_head children;//指向子进程
	struct list_head sibling;//指向兄弟进程
	struct list_head {
		struct list_head *next,*prev;
	};

一个进程的父进程有两个,真正父进程real_parent是创建出当前进程的进程,而父进程parent是与信号响应相关的父进程(本进程的终止信号会被发送到parent,真正父进程没有终止的情况下,真正父进程和父进程就是同一个进程,如果真正父进程先终止了,就会有init进程成为当前的父进程)。

2.1.2用来控制进程的信息
  1. PCB当中的进程控制信息主要包括进程的状态信息(进程是描述进程处于就绪/执行/阻塞/终止哪个状态)
  2. 进程优先级信息(描述这个进程的优先级怎样,包括静态优先级(一开始就给定)、动态优先级(会受进程调度策略影响而被临时修改)、普通优先级(普通优先级不区分实时进程和普通进程,按照原来的静、动态优先级的顺序执行)、实时优先级(实时优先级会区分普通进程和实时进程,实时进程优先))
  3. 以及记账信息等。这包括进程占用和利用的资源情况(占有CPU的时钟周期数以及占用CPU时间总和),以此为依据对进程进行调度和控制。openEuler的记账信息是这样的,其中还对进程切换时间进行了计数:
//源文件:include/linux/sched.h
u64 utime//进程在用户态下占用的 CPU 时钟周期数
u64 stime;//进程在内核态下占用的 CPU 时钟周期数
u64 utimescaled//记录进程在用户态下的运行时间
u64 stimescaled;//记录进程在内核态下的运行时间
u64 gtime;//虚拟机运行的 CPU 时钟周期数
u64 start_time//进程创建时间
u64 real_start_time;//进程创建时间,还包括进程睡眠时间
unsigned long nvesw, nivcsw;//上下文切换计数
2.1.3 进程切换时需要的CPU上下文

CPU上下文是指当进程执行至某时刻时CPU各寄存器的值,进程切换会丧失对CPU的控制权,所以当进程切换时,操作系统要保存进程在切换时的CPU上下文以便恢复被打断之前的状态。于是在PCB的cpu_context中定义了通用寄存器、栈帧寄存器FP、堆栈指针寄存器SP\程序计数器PC等。

2.1.4 进程对内存和文件相关资源的管理信息

记录了进程的内存布局、与文件系统相关联的信息(当前目录和根目录、进程打开的所有文件的列表)

//源文件:include/1inux/sched.h
void * stack;//指向进程的内核栈
struct mm_struct * mm,*active_mm;//进程的用户空间描述符
struct fs_struct * fs;//进程相关联的文件系统信息
struct files_struct * files;//指向打开的文件列表

//源文件:include/linux/mm_types.h
struct mm_struct {//内存描述符
spinlock_t arg_lock;//自旋锁,保护下面这些字段
//内存空间中各段起始/结束地址
//包括栈、映射段、堆、BSS 段、数据段、代码段
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
};

//源文件:include/linux/fs_struct.h
struct fs_struct {//文件系统描述符
int users;//该结构的引用用户数
spinlock_t lock;//自旋锁
struct path root, pwd;//根目录与当前目录

//源文件:include/linux/fdtable.h
struct files_struct {
atomic_t count;//引用计数
struct fdtable __rcu * fdt;//默认指向 fdtab,可用于动态申请内存
struct fdtable fdtab;//为 fdt 提供初始值
};

2.2 进程的不同状态

进程有四个状态:分别是

  1. 就绪(进程的资源分配完毕,并位于运行队列中,等待CPU资源给到本进程去占用)、
  2. 运行、(一旦进程占用CPU资源就进入运行状态,)
  3. 阻塞、(外部条件不满足而不能继续执行,就要接触对CPU资源的占用,等待外部条件(如等待输入、等待前驱事件发生、等待其它外部设备资源释放等)满足后就能继续执行的状态;如果条件满足之前就进入了下一个时间片,那进程就只能等待CPU资源的分配,这个时候就会转换到就绪状态了)。阻塞和就绪不同的是阻塞是等待外部条件满足就执行,就绪等待的是CPU资源。
    在阻塞状态时,除了等到外部条件的满足能唤醒这个进程继续执行外,还可以通过其他方式唤醒这个进程:被系统调用(system call)显式的或急需处理的信号唤醒(能这样唤醒的一般是轻度的阻塞状态);而中度阻塞能被显式地唤醒或被致命的信号(如能让进程终止的信号);至于深度阻塞状态就只能被显式地唤醒,不会因信号退出阻塞状态。
  4. 终止
    当进程不再执行就会进入终止状态,特别地,当自身和占用的资源全部被操作系统回收时,进程就会进入死亡状态。但是如果进程没有死透(即在父进程那里留下了它的痕迹或者占用的资源没有解除占用),这些资源没有被回收就会成为僵尸进程。

如何控制进程

今天先肝到这,等书回来了2.2号到2.16之后继续操作系统深入探究。。。

你可能感兴趣的:(服务器,linux,运维,系统架构)