操作系统(二) 进程

目录

知识导图

一、进程

二、进程控制

(一)进程控制块PCB

(二)进程的控制

1. 创建进程

2. 终止进程

3. 阻塞进程

4. 唤醒进程

(三)上下文切换

1. CPU上下文

2. 进程的上下文切换

三、进程同步

(一)临界区

(二)互斥量

(三)信号量

使用信号量实现生产者-消费者问题

(四)管程

四、进程间通信

(一)管道

(二)消息队列

(三)共享内存

(四)信号量

(五)Socket套接字

(六)信号

参考资料


 

知识导图

一、进程

简单来说,进程指的就是运行中的程序,它是一个程序及其数据在处理机上顺序执行时发生的活动,是系统进行资源分配和调度的一个独立单位。一个进程具有以下几个状态:

  • 就绪(Ready):已经获得除CPU外其他所有必要资源,处于准备好运行的状态;
  • 运行(Running):程序正在执行,占用CPU资源;
  • 阻塞(Block):该进程正在等待某一事件发生(如I/O请求)而暂时停止运行,此时即使给它CPU控制权,它也无法运行;
  • 创建(New):进程创建所需资源还不能得到满足,创建未完成时称为创建状态;
  • 终止(Exit):进程结束
  • 挂起(Suspend):表示进程没有占有物理内存空间,是由于系统或用户的需要而采取的操作(如用户希望程序停止以便进行修改)
    • 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;

    • 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行;

 

进程状态的切换过程如下:

图片

 

二、进程控制

(一)进程控制块PCB

为了便于系统描述和管理进程,OS为每个进程定义了一个数据结构——进程控制块(process control block,PCB),用来记录进程当前状态及进程管理所需信息。一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失。PCB包含的信息如下:

1. 进程描述信息:

  • 进程标识符:标识各个进程,每个进程都有一个并且唯一的标识符;

  • 用户标识符:进程归属的用户,用户标识符主要为共享和保护服务;

2. 进程调度信息:

  • 进程当前状态:如 new、ready、running、waiting 或 blocked 等;

  • 进程优先级:进程抢占 CPU 时的优先级;

  • 事件:进程由执行转为阻塞时所发生的事件

  • 其他信息:以供进程调度算法使用

3. 资源分配清单:

  • 有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的 I/O 设备信息。

4. CPU 相关信息:

  • CPU 中各个寄存器的值,当进程被切换时,CPU 的状态信息都会被保存在相应的 PCB 中,以便进程重新执行时,能从断点处继续执行。

PCB的组织方式

(1)线性方式

将所有PCB存储在一张线性表中,内存开销小,但每次查找都需扫描整张表(类比数组),适用于进程数目不多的系统。

(2)链表方式

把具有相同状态的进程链在一起,组成各种队列。比如:

  • 将所有处于就绪状态的进程链在一起,称为就绪队列

  • 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列

(3)索引方式

将同一状态的进程组织在一个索引表中,索引表项指向相应的 PCB,不同状态对应不同的索引表。

 

一般会选择链表,因为可能面临进程创建,销毁等调度导致进程状态发生变化,所以链表能够更加灵活的插入和删除。

 

(二)进程的控制

1. 创建进程

创建进程的过程如下:

  • 为新进程分配一个唯一的进程标识号,并申请一个空白的 PCB,PCB 是有限的,若申请失败则创建失败;

  • 为进程分配资源,此处如果资源不足,进程就会进入等待状态,以等待资源;

  • 初始化 PCB;

  • 如果进程的调度队列能够接纳新进程,那就将进程插入到就绪队列,等待被调度运行;

操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源,当子进程被终止时,其在父进程处继承的资源应当还给父进程。同时,终止父进程时同时也会终止其所有的子进程。

2. 终止进程

终止进程的过程如下:

  • 根据进程标识号查找需要终止的进程的 PCB,读取进程状态;

  • 如果处于执行状态,则立即终止该进程的执行,然后将 CPU 资源分配给其他进程;

  • 如果其还有子进程,则应将其所有子进程终止,以防它们成为不可控进程;

  • 将该进程所拥有的全部资源归还给父进程或操作系统;

  • 将其从 PCB 所在队列中删除

进程可以有 3 种终止方式:正常结束异常结束以及外界干预(信号 kill 掉)。

3. 阻塞进程

当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待,它只能由另一个进程唤醒。

阻塞进程的过程如下:

  • 找到将要被阻塞进程标识号对应的 PCB;

  • 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行;

  • 将该 PCB 插入的阻塞队列中去;

4. 唤醒进程

唤醒进程的过程如下:

  • 在该事件的阻塞队列中找到相应进程的 PCB;

  • 将其从阻塞队列中移出,并置其状态为就绪状态;

  • 把该 PCB 插入到就绪队列中,等待调度程序调度;

进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句。

 

(三)上下文切换

各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执行,这种一个进程切换到另一个进程运行的行为,称为进程的上下文切换

1. CPU上下文

在每个任务运行前,CPU 需要知道任务从哪里加载,又从哪里开始运行。CPU 在运行任何任务前,所必须依赖的环境就叫CPU的上下文。

CPU的上下文包括 CPU寄存器程序计数器(PC)

CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

2. 进程的上下文切换

进程是由内核管理和调度的,所以进程的切换只能发生在内核态。

进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:

图片

 

三、进程同步

为了保证多个进程能有条不紊地并发运行,必须引入进程同步机制。它使得并发的进程之间能按照一定规则或时序共享系统资源。

(一)临界区

对临界资源进行访问的那段代码称为临界区。

为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。

如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

// entry section
// critical section;
// exit section

(二)互斥量

互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥可以在不同应用程序的线程之间实现对资源的安全共享。

  • 同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。
  • 互斥:多个进程在同一时刻只有一个进程能进入临界区。

(三)信号量

信号量(Semaphore)是一个整型变量,代表同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。

可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。

  • down : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;
  • up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。

down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。

如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。

typedef int semaphore;
semaphore mutex = 1;
void P1() {
    down(&mutex);
    // 临界区
    up(&mutex);
}

void P2() {
    down(&mutex);
    // 临界区
    up(&mutex);
}

使用信号量实现生产者-消费者问题

问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。

因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。

为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。

注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。

#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;

void producer() {
    while(TRUE) {
        int item = produce_item();
        down(&empty);
        down(&mutex);
        insert_item(item);
        up(&mutex);
        up(&full);
    }
}

void consumer() {
    while(TRUE) {
        down(&full);
        down(&mutex);
        int item = remove_item();
        consume_item(item);
        up(&mutex);
        up(&empty);
    }
}

(四)管程

管程是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。对于请求访问共享资源的诸多并发进程,管程可以根据资源的情况接受或阻塞,确保每次仅有一个进程进入管程执行。

管程引入了 条件变量 以及相关的操作:wait() 和 signal() 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。

 

四、进程间通信

进程之间有时需要传输大量数据,而每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。

(一)管道

管道类似于一种进程间共享的文件,它存在于内存中,进程可以对它进行读写,它提供流控制,保证进程的正确读写,即管道为空时读进程会阻塞,管道为满时写进程会阻塞,以此实现进程之间的通信。

  管道分三种:

  • 匿名管道(普通管道,也常直接称管道):只能在父子进程之间使用;
  • 流管道:相对于普通管道而言,它不止是单向传输,可以双向传输。
  • 命名管道(FIFO):可以在不相关的进程间也能相互通信。

管道的通信方式效率低,因此管道不适合进程间频繁地交换数据。

(二)消息队列

消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),当 A 进程要给 B 进程发送消息时,A 进程把数据放在对应的消息队列后就可以正常返回了。它实现了消息的随机读取,且独立于进程存在,进程终止时,消息队列及其内容并不会被删除。

消息队列存在不足的地方有两点,一是通信不及时,二是数据也有大小限制。

而且消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程。

(三)共享内存

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,不需要拷贝来拷贝去,大大提高了进程间通信的速度。共享内存是几种通信方式中最快的一种。

图片

共享内存通信方式存在一个问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。

(四)信号量

为了防止共享内存中多进程竞争共享资源而造成数据错乱,所以需要保护机制,使得共享的资源在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。

信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步。

信号量表示资源的数量,控制信号量的方式有两种原子操作:

  • 一个是 P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。

  • 另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;如果信号量 > 0,则表明当前没有阻塞中的进程;

P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。

举个例子,如果要使得两个进程互斥访问共享内存,我们可以初始化信号量为 1

具体的过程如下:

  • 进程 A 在访问共享内存前,先执行了 P 操作,由于信号量的初始值为 1,故在进程 A 执行 P 操作后信号量变为 0,表示共享资源可用,于是进程 A 就可以访问共享内存。

  • 若此时,进程 B 也想访问共享内存,执行了 P 操作,结果信号量变为了 -1,这就意味着临界资源已被占用,因此进程 B 被阻塞。

  • 直到进程 A 访问完共享内存,才会执行 V 操作,使得信号量恢复为 0,接着就会唤醒阻塞中的线程 B,使得进程 B 可以访问共享内存,最后完成共享内存的访问后,执行 V 操作,使信号量恢复到初始值 1。

可以发现,信号初始化为 1,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问,这就很好的保护了共享内存。信号初始化为 0,就代表着是同步信号量,它可以保证进程 A 应在进程 B 之前执行。

(五)Socket套接字

前面提到的几种通信方式都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。

关于Socket通信,详见Socket通信原理

(六)信号

上面的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程

信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,进程就可以对信号采用相应的处理方式。

 

 

参考资料

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值