操作系统-进程管理

1. 进程概念

1.1 进程基本概念

我们知道OS可能会在任何时候暂停或是继续一个程序的运行。
在这里插入图片描述
程序在并发环境中可能存在以下的问题:

  • 运行过程不确定
  • 结果不可再现(程序运行被干扰)

因此OS引入了进程的概念。那什么是进程呢?进程其实就是程序在某个数据集合上的一次运行活动。简单点说,进程就是运行中的程序,它是一个动态的概念

进程的特征
  • 动态性:进程是程序的一次执行过程
  • 并发性:进程同其他进程一起向前推进
  • 异步性:进程按照各自的速度向前推进
  • 独立性:进程是系统分配资源和调度CPU的单位(进程是CPU分配资源的最小单位)
进程程序
动态:进程是程序的一次执行过程静态
暂存:在内存驻留长存:在硬盘存储
一个程序可能有多个进程
1.2 进程状态
  • 运行状态:进程已经占有CPU,在CPU运行
  • 就绪状态:具备运行条件,但是没有CPU,暂时无法运行
  • 阻塞状态:因为等待某项服务完成或信号不能运行的状态,如系统调用、I/O操作等
1.2.1 进程的状态变迁

进程的状态可以根据一定的条件相互转化

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1.3 进程控制块PCB

进程控制块PCB是描述进程状态、资源和与相关进程关系的数据结构。PCB在进程创建时创建,在进程撤销后PCB也同时撤销。

我们可以这样认为:进程 = 程序 + PCB

1.3.1 PCB的基本成员
  • name(ID):进程名称
  • status:状态
  • next:指向下一个PCB的指针
  • priority:优先级
  • cpu_status:现场保留区(堆栈)
  • comm_Info:进程通信
  • process_family:家族
  • own_resource:资源

Linux的PCB是有task_struct描述的。
在这里插入图片描述

Linux的进程标识:

  • PID:进程ID
  • PPID:父进程ID
  • PGID:进程组ID

Linux进程的用户标识:

  • UID:用户ID
  • GID:用户组ID
1.3.2 进程的切换

首先说明一下进程的上下文Context,Context指的是进程的运行环境即CPU环境。

进程的切换过程:

  • 换入进程的上下文进入CPU(从栈上来)
  • 换出进程的上下文离开CPU(从栈上去)
1.3.3 进程创建

创建一个具有指定标识(ID)的进程,其参数包括:

  • 进程标识、优先级、进程起始地址、CPU初始状态、资源清单等

进程创建过程:

  • 创建一个空白PCB
  • 获得并赋予进程标识符ID
  • 为进程分配内存空间
  • 初始化PCB(默认值)
  • 插入相应的进程队列(新进程插入就绪队列)
1.3.4 进程撤销

进程撤销的时机

  • 正常结束
  • 异常结束
  • 外界干预

需要的参数有

  • 被撤销进程名(ID)

进程撤销过程:

  • 在PCB队列中检索出该PCB
  • 获取该进程的状态
  • 若该进程处于运行态,立即终止该进程(递归检查是否有子进程,先撤销子进程
  • 释放进程占用的资源
  • 将进程从PCB队列中移除
1.3.5 进程阻塞

停止进程的执行,变为阻塞。

阻塞的时机:

  • 请求系统服务(由于某种原因,OS不能立即满足进程的要求)
  • 启动某种操作(进程启动某种操作,阻塞等待该操作完成)
  • 新数据尚未到达(A进程要获取B进程的中间结果,A进程等待)
  • 无新工作可做(进程完成任务,自我阻塞,等待新任务)

阻塞的实现过程:

  • 停止进程进程
  • 将PCB“运行态“改为“阻塞态”
  • 插入相应原因的阻塞队列
  • 转调度程序
1.3.6 进程唤醒

唤醒处于阻塞队列当中的某个进程。

唤醒时机:

  • 系统服务不满足到满足
  • I/O完成
  • 新数据到达
  • 进程提出新请求(服务)
1.3.7 进程控制原语

原语:

  • 由若干指令构成的具有特定功能的函数
  • 具有原子性,其操作不可分割

进程控制原语包括:

  • 创建原语
  • 撤销原语
  • 阻塞原语
  • 唤醒原语

2. 进程控制

2.1 Windows进程控制
函数描述
CreateProcess()创建进程
ExitProcess(UINT uEixtCode)结束进程
TerminateProcess(hProcess, uExitCode)结束进程
2.2 Linux进程控制

pid_t pid = fork() :创建进程

  • 父进程是当前进程的子进程
  • 父进程:fork()的调用者;子进程:新建的进程
  • 子进程是父进程的复制
  • 父进程和子进程并发运行

fork返回值pid有

  • 子进程中 pid = 0
  • 父进程中 pid > 0
  • 出错 pid = -1

分析下面的代码:

int main(void){
	pid_t pid;
	pid = fork()
	//子进程将会执行fork()一下的所有代码
	
	if(pid == 0) printf("child process");
	else if(pid > 0) printf("parent process");
	else printf("error");
}

上述代码将会输出
child process
parent process

或者
parent process
child process

do_fock()函数的实现
//fork()函数详解
//代码来自github:https://github.com/torvalds/linux/blob/master/kernel/fork.c

/*
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 *
 * args->exit_signal is expected to be checked for sanity by the caller.
 */
long _do_fork(struct kernel_clone_args *args)
{
	u64 clone_flags = args->flags;
	struct completion vfork;
	struct pid *pid;
	struct task_struct *p; 
	int trace = 0;
	long nr;

	/*
	 * Determine whether and which event to report to ptracer.  When
	 * called from kernel_thread or CLONE_UNTRACED is explicitly
	 * requested, no event is reported; otherwise, report if the event
	 * for the type of forking is enabled.
	 */
	if (!(clone_flags & CLONE_UNTRACED)) {
		if (clone_flags & CLONE_VFORK)
			trace = PTRACE_EVENT_VFORK;
		else if (args->exit_signal != SIGCHLD)
			trace = PTRACE_EVENT_CLONE;
		else
			trace = PTRACE_EVENT_FORK;

		if (likely(!ptrace_event_enabled(current, trace)))
			trace = 0;
	}
	
	//把当前进程task_struct结构中所有内存拷贝到新进程中
	p = copy_process(NULL, trace, NUMA_NO_NODE, args);
	add_latent_entropy();
	
	//判断进程拷贝是否成功,如果失败则返回错误
	if (IS_ERR(p))
		return PTR_ERR(p);

	/*
	 * 在唤醒新线程前执行此操作 - 如果线程迅停止那线程指针可能会失效
	 */
	trace_sched_process_fork(current, p);
	
	//获取新(子)进程的ID
	pid = get_task_pid(p, PIDTYPE_PID);
	nr = pid_vnr(pid);

	if (clone_flags & CLONE_PARENT_SETTID)
		put_user(nr, args->parent_tid);

	if (clone_flags & CLONE_VFORK) {
		p->vfork_done = &vfork;
		init_completion(&vfork);
		get_task_struct(p);
	}

	wake_up_new_task(p); //唤醒进程,将其挂入可执行队列等待被调度

	/* forking complete and child started to run, tell ptracer */
	if (unlikely(trace))
		ptrace_event_pid(trace, pid);

	if (clone_flags & CLONE_VFORK) {
		if (!wait_for_vfork_done(p, &vfork))
			ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
	}

	put_pid(pid);
	return nr;
}
系统调用描述
forkfork创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容task_struct内容
vforkvfork创建的子进程与父进程共享数据段,而且由vfork()创建的子进程将先于父进程运行
cloneLinux上创建线程一般使用的是pthread库 实际上linux也给我们提供了创建线程的系统调用,就是clone

系统调用服务例程sys_clone, sys_fork, sys_vfork三者最终都是调用do_fork函数完成.

三者的区别和关联请参考 https://blog.csdn.net/gatieme/article/details/51417488

3. 线程

在这里插入图片描述
python实现代码:

import queue,random,threading
from time import sleep
 
def drawRect():
    print('开始画矩形')
    for i in range(10):
        print('画矩形中')
    print('完成矩形')
 
def drawCircle():
    print('开始画圆')
    for i in range(10):
        print('画圆中')
    print('完成画圆')
 
 
if __name__ == '__main__':
    t1 = threading.Thread(target=drawRect)
    t2 = threading.Thread(target=drawCircle)

    t1.start()
    t2.start()

    t1.join()
    t2.join()
    

单线程和多线程程序:

  • 单线程程序:整个进程只有一个线程。windows程序确实只有一个主线程
  • 多线程程序:整个进程至少有两个线程。主线程和至少一个用户线程
线程的应用场景
  • 程序的功能需要并发运行(如同时画圆和画方)
  • 提高窗口程序的交互性
  • 多核CPU的应用,重复发挥性能

在这里插入图片描述
windows创建线程:CreateThread()
Linux创建线程:pthread_create()

使用线程的问题
  • 程序难以调试
  • 并发过程难以控制
  • 线程安全问题

4. 临界区和锁

4.1 临界资源和临界区

在这里插入图片描述

4.1.1 什么是临界区和临界资源
  • 临界资源:一次只允许一个进程独占访问的资源,例如:上图中的共享变量i
  • 临界区:进程中访问临界资源的程序段。并发进程不能同时进入临界区。
4.1.2 设计临界区访问机制的四个原则
  • 忙则等待:临界区忙时,其他进程必须在临界区外等待
  • 空闲让进:当无进程在临界区,则任何有权进程可进入临界区
  • 有限等待:进程进入临界区的请求应在有限时间内得到满足,这就涉及到临界区的大小问题。
  • 让权等待:等待进程放弃CPU,让其他进程有机会得到CPU
4.2 锁机制
4.2.1 锁机制原理

设置一个标志S, 则有 S == 1 ? "临界资源可用" : "临界资源不可用"

上锁操作:

  • 检查S是否可用
  • 若为不可用状态,则进程在临界区外等待
  • 若可用,则访问临界区资源,且将S修改位不可用状态

开锁操作:

  • 退出临界区将S改为可用状态
LOCK(S){
	TEST: if(S == 0)
		goto TEST; //测试锁状态
		else
			S = 0
}

UNLOCK(S){
	S = 1;//开锁
}
4.2.2 用锁机制访问临界区

步骤:

  • 初始化锁状态S = 1(可用)
  • 进入临界区前执行上锁LOCK操作
  • 离开临界区后执行开锁UNLOCK操作

在这里插入图片描述

5. 同步和P-V操作

5.1 同步和互斥概念
5.1.1 进程同步

若干个进程为了完成一个共同的任务,需要相互协调运行步伐。一个进程开始某个操作之前必须要求另一个进程已经完成某个操作,否则前面的进程只能等待。

进程同步关系的例子:司机和售票员

  • 司机:起步,行驶,停车
  • 售票员:关门,售票,开门
    同步关系
  • 司机起步前售票员要先关门,否则等待
  • 售票员开门前司机要先停车,否则等待
5.1.2 进程互斥

多个进程由于共享了独占性支援,必须协调各进程对资源的存取顺序,确保没有任何两个或以上的进程同时进行存取操作。互斥和资源共享有关。这个存取操作区域为临界区。

5.2 P-V操作
5.2.1 信号灯概念

信号灯是一种卓有成效的进程同步机制。由Dijkstra于1965年提出。进程在运行过程中受信号灯控制,并能改变信号灯状态。

信号灯数据结构

  • 信号灯变量定义为一个二元矢量(S, q)
  • S:整数,初值为负。S又称信号量
  • q:PCB队列,初值为空集
struct SEMPHORE
{
	int S;
	pointer_PCB q;
}

信号灯的操作有两种:

  • P操作(通过):P(S, q)
  • V操作(释放):V(S, q)
5.2.2 信号灯的操作
P操作V操作
1S--S++
2S >= 0,进程继续S > 0,进程继续
3S < 0 ,进程阻塞并加入到队列q中,并转调度函数S <= 0 ,该进程继续,同时从q中唤醒一个进程
P操作可能使进程在调用处阻塞。可能唤醒阻塞的进程
5.3 P-V操作解决互斥问题

解决互斥实质是实现对临界区的互斥访问,即允许最多一个进程处于临界区

应用过程

  • 设定合理的S初值
  • 进入临界区前执行P操作
  • 离开临界区之后执行V操作

分析Pa,b,c三者互斥过程,代码如下:

main(){
	int mutex = 1;
	cobegin //并发
		Pa();
		Pb();
		Pc();
	coend();//并发结束
}

Pa(){
	P(mutex);
	CS;//访问临界区
	V(mutex);
}

Pb(){
	P(mutex);
	CS;//访问临界区
	V(mutex);
}

Pc(){
	P(mutex);
	CS;//访问临界区
	V(mutex);
}

分析,

PaPb
S = 1S = 1
Pa运行Pb运行
假设Pa进程先执行,则S--,所以S = 0Pb执行时,S = 0,所以阻塞
S >= 0,所以Pa进程继续阻塞
Pa进入临界区阻塞
S++,所以S = 1阻塞
S > 0,Pa进程继续,同时唤醒阻塞进程Pb被唤醒
Pa继续执行S--
Pa继续执行S = 0,所以Pb进程继续执行
5.4 P-V操作解决同步问题

基本思路

  • 定义有意义的信号量S,设置合适的初值。S能明确的表示运行条件。
  • 暂停当前进程:在关键操作执行之前执行 P 操作
  • 继续进程:在关键操作之后执行 V 操作

继续以司机和售票员为例:

  • 司机:起步,行驶,停车
  • 售票员:关门,售票,开门

同步关系

  • 司机起步前售票员要先关门,否则等待
  • 售票员开门前司机要先停车,否则等待

即:售票员关门 -> 司机起步 -> 司机行驶,售票员售票 -> 司机停车 -> 售票员开门-> 售票员关门 -> …

import threading
import time

s_door = threading.Semaphore(0) # 门是否关好
s_car = threading.Semaphore(0)  # 车是否停稳

def driver():
    for i in range(2):
        s_door.acquire() # P操作,意味着分配资源。获取关好门的信号
        print('司机起步')
        print('司机行驶')
        time.sleep(1)
        print('司机停车')
        s_car.release()  # V操作,意味着释放资源。发出车停稳的信号
        
def seller():
    for i in range(2):
            print('售票员关门')
            s_door.release() # V操作,发出关好门的信号
            time.sleep(1)
            print('售票员售票')
            s_car.acquire() # P操作,获取车停稳的信号
            print('售票员开门')
if __name__=='__main__':
    p1 = threading.Thread(target=driver)
    p2 = threading.Thread(target=seller)
    p1.start()
    p2.start()
    p1.join()
    p2.join()

总结:

  • 关键操作之前 P 操作
  • 关键操作之后 V 操作

上例中,司机起步和售票员开门是关键操作,因此在这些操作前 P 操作,之后 V 操作。

  • 售票员开门会使S1从 1 -> 0
  • 司机起步会使S2从1 -> 0
经典同步问题1:生产者和消费者问题

一群生产者向一群消费者提供产品,共享缓冲区,如下图示。规则是

  • 不能向缓存区产品
  • 不能从缓冲区产品
  • 每个时刻仅允许1个生产者消费者 1个产品
    在这里插入图片描述
int FULL = 0; 	// 缓冲区数据个数,初值为0
int EMPTY = 5; 	// 缓存区空位个数,初值为5
int S3 = 1; 	// 是否允许用户存或取,互斥使用

Producer i(){
	while(TRUE)
	{
		P(EMPTY);
		P(S3);
		存产品 //存产品会使得 EMPTY从非0到0
		V(S3);
		V(FULL);
	}
}

Consumer j(){
	while(TRUE)
	{
		P(FULL);
		P(S3);
		取产品 //取产品会使得 FULL从非0到0
		V(S3);
		V(EMPTY);
	}
}
import queue,random,threading
from time import sleep
 
mutex = threading.Semaphore(1)      # 临界区互斥信号量
s_empty = threading.Semaphore(5)    # 缓冲区空闲的信号量
s_full = threading.Semaphore(0)     # 缓冲区满了的信号量
 
 
def productor(i, q):
    while True:
        num = random.choice(['华为P30', '小米9', 'ViVo x27', 'iphone XR'])
        print('生产者%d生产了产品%s' % (i, num))
        s_empty.acquire()   # 缓冲区是否有空闲
        mutex.acquire()     # 获取缓冲区无人访问信号
        q.put(num)
        sleep(1)
        print('生产者%d把产品%s放入了仓库中' % (i, num))
        mutex.release()
        s_full.release()
 
 
 
def consumer(i, q):
    while True:
        s_full.acquire()
        mutex.acquire()
        num = q.get()
        sleep(1)
        print('消费者%d购买了产品%s' % (i, num))
        mutex.release()
        s_empty.release()
 
 
 
if __name__ == '__main__':
    q = queue.Queue(5)  # 创建上限为5的缓冲区
 
    # 创建2个生产者
    for i in range(2):
        threading.Thread(target=productor, args=(i, q)).start()
 
    # 创建3个消费者
    for i in range(3):
        threading.Thread(target=consumer, args=(i, q)).start()
    
经典同步问题2:读者和编者问题

问题描述:有一本书

  • 有读者读书;有多个读者
  • 有编者编书;有多个编者

要求:

  • 允许多个读者同时读书:读者间不互斥
  • 不允许多个编者同时编书:编者间互斥
  • 不允许读者编者同时操作:编者和读者间互斥
int RNUM = 0; 		// 读者个数
int r_mutex= 1;		// 临界资源RNUM的互斥访问
int e_r_mutex = 1; 	// 编者和读者互斥,可认为是允许编者或读者访问

Reader i(){
	while(TRUE)
	{
		P(r_mutex);//保证以下操作的原子性
		RNUM++;
		if(RNUM == 1) P(e_r_mutex )
		V(r_mutex );
		
		读书;
		
		P(r_mutex);
		RNUM--;
		if(RNUM == 0) V(e_mutex)
		V(r_mutex);
	}
}

Editor j(){
	while(TRUE)
	{
		P(e_r_mutex );
		编书
		V(e_r_mutex );
	}
}

6. Windows同步机制

互斥量(Mutex)

  • 保证一个线程或进程可以申请到该对象
  • 可以跨进程使用
  • 可以有名称
  • 互斥量比临界区资源耗费更多资源,速度慢

信号量(Semaphore)

  • 允许指定数目的多个进程或线程访问临界区
  • 是一种资源计数器,用于限制并发线程的数量
  • 初始值设置为N,则代表允许N个进程或线程访问资源

在这里插入图片描述

7. Linux同步机制

  • 对于普通变量,父子进程各自操作变量副本,互相不影响
  • 对文件资源,父子进程共享同一文件和读写指针

8. 进程通信

8.1 管道通信
8.1.1 管道通信机制

管道是进程间的一种通信机制。一个进程A可以通过管道将数据传输给另一个进程B。
程序A写数据 =====管道=====> 程序B读数据

管道通信仅能用于父子或兄弟进程间通信。如果用进行双写通信,则需要建立2个管道。

8.2 Linux信号(Signal)通信

信号的概念:

  • 信号是Linux进程间的一种重要通信机制
  • 信号是向进程发送一个通知,通知某个事件已发生
  • 收到信号的进程可以立即执行指定的操作
  • 信号的发出可以是进程,也可以是系统

信号产生的例子:

  • 键盘按下 Ctrl + C,产生SIGINT信号,杀死一个进程
  • 键盘按下 Ctrl + Z,产生SIGTSTP信号,暂停一个进程
  • kill -9 产生SIGKILL信号,强制结束进程
  • 程序调用函数产生信号,如kill(), abort()
  • 硬件异常或者内存产生相应信号,如内存访问错误
    在这里插入图片描述

Signal函数原型:

void signal(
	int SigNo, 			// 信号编号
	void (* Handle) int // 自定义信号处理函数
)

kill(): 发送信号的函数

/*
* 向目标进程PID发送SigNo信号
*/
void kill(
	int PID, 	// 接受信号的目标进程ID
	int SigNo 	//待发送的信号
)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值