Log[操作系统笔记_PART2]_20.05.23

第二章 进程与线程

2.1 进程

2.1.1 什么是进程

计算机上所有可以运行的软件,当它开始工作时,它就是运行在计算机上的一个进程。进程有它自己独占的地址空间。在多道程序设计系统中,内存中可以同时存在多道进程,按照一定的调度规则获得CPU执行,所以就需要一个功能,这个功能记录每个进程释放CPU时未运行的第一条指令。具体实现是:每个进程都有自己的逻辑程序计数器,在进程运行时,它的逻辑程序计数器被装入实际的物理程序计数器中;在离开CPU时,将物理程序计数器的内容存放到内存中的本进程的逻辑程序计数器中。

UNIX中可以形成一棵进程树,Windows中进程没有层次划分结构。

进程的五种状态:新建——>就绪——>运行——>阻塞——>销毁

操作系统最底层是调度程序,在它上面有许多进程。所有的中断处理、启动和停止进程都由调度程序来进行运作。

2.1.2 如何创建进程

1)启动阶段创建

2)程序执行了创建进程的系统调用(进程自主)

UNIX中: fork()函数就是创建新进程的系统调用,调用过程是首先会创建一个与调用进程相同的副本,此时两个进程拥有相同的内存映像(存在于内存上,是对实际物理地址的一个映射)同样的环境字符串和同样的打开文件;接着子进程执行execve()用来修改内存映像并运行一个新程序。

Windows中:CreateProcess函数直接执行创建和装入程序的工作

3)用户发出创建进程的请求,比如在交互式系统中点击图标(用户自主)

4)批处理作业的初始化

结束进程:进程运行完毕后会执行一个系统调用(exit),通知操作系统工作已完成

2.1.3 进程的内部实现

操作系统维护着一张表——进程表。每个进程占用一个表项。表项的主要内容有:程序计数器、堆栈指针、内存分配情况、打开文件的状态、账号和调度信息,还有退出运行态时为下一次恢复现场而保存的信息(寄存器、使用的CPU时间等等)

eg:在中断发生时,由硬件将正在执行的进程的程序计数器、寄存器压入堆栈,计算机跳到中断向量所指示的地址,接着软件执行中断服务。执行完毕后使某些进程就绪,按照调度程序选择进程获得CPU,接着控制权转给一段汇编代码,为当前进程装入寄存器值以及内存映射并启动该进程。

2.2 线程

2.2.1 线程的简介及使用

线程是包含在进程内的,一个进程可以有多个线程,线程共享地址空间,并且可以并行运行。线程是CPU上被调度执行的实体。在很多场景中,多线程同时运行可以提高程序性能。

eg:有一个字处理软件,用户修改了第五页的某个地方,然后跳转到第500页,这时候由于已经被修改,程序就要对文件重新进行格式处理才能准确找到第500页,从用户的角度来看响应就会缓慢。如果采用多线程,在用户修改完毕后就立即用一个线程进程格式处理,然后用户再去找第500页的时候,就能及时得到响应。再拓展一下,为了防止断电导致文件丢失,还可以创建一个线程用来定时自动处理磁盘备份。

以上这个例子说明了多线程在效率提升方面的好处,而且也说明了这些工作只有多线程能做,因为三个线程处理的是同一个文件,正好符合了线程共享进程空间的特性。

2.2.2 线程模型

线程拥有程序计数器、寄存器(保存当前工作变量)、堆栈(每一帧都保存了一个已经调用但还没有执行完的过程)。同时每个线程还共享进程的地址空间、全局变量、子进程等。

属于进程

属于线程

地址空间

全局变量

打开文件

子进程

即将发生的定时器

信号与信号处理程序

账户信息

程序计数器

寄存器

堆栈

状态

2.2.3 POSIX线程

线程包pthread,定义了一系列和线程有关的操作。

pthread_creat,新建线程的线程标识符作为返回值返回。

pthread_exit 终止该线程并释放它的栈

pthread_join、pthread_yield 跟Java中的方法差不多作用

pthread_attr_init 建立关联这个线程的属性结构并赋予默认值 比如它的优先级信息等

pthread_attr_destory 删除线程的属性结构 (这两个跟Java的设置优先级方法差不多)

2.2.4 用户空间实现线程与内核空间实现线程

2.2.4.1 用户空间实现

把整个线程包放在用户空间中,内核管理进程时仍然当作单线程进程处理。在用户空间的每个进程都有一张线程表,记录着各个线程的信息(见上表)。当线程就绪或阻塞时,在线程表中存放重新启动线程的信息。

2.2.4.2 内核空间实现

在内核系统中记录所有线程的线程表。在创建新的线程时,需要执行系统调用,这个系统调用更新线程表以完成对线程的创建。与用户空间实现线程不同的是:在一个线程阻塞的时候,内核可以运行任意一条线程,就算线程不属于同一个进程。

在内核空间创建撤销线程代价比较大,所以使用回收线程:在某个线程撤销时,标记为不可使用但不改变其属性结构,稍后在创建新线程时重新启动一个旧线程。

2.2.4.3 两者优缺点

用户空间优点:1)进行线程调度不用切换至内核态 2)允许每个进程有自己的调度算法

用户空间缺点:如果线程出现页面故障,操作系统在去磁盘取丢失指令的时候会阻塞整个进程。

内核空间优点:在发生页面故障时,内核可以检查这个进程是否有其他可运行的线程,如果有,在磁盘读入的同时选择可运行的线程执行。

内核空间缺点:如果线程的创建、销毁比较多,那么开销会比较大

2.3 进程间通信

2.3.1 先导概念

竞争条件:两个或者多个进程读写某些共享数据,最后的结果取决于进程运行的顺序,这种情况就形成了竞争条件。

临界区:程序内对共享内存进行访问的代码,就叫临界区。要避免竞争条件,就要事两个进程不同时处于临界区内,也就是下面所描述的实现互斥。

2.3.2 忙等待的互斥

2.3.2.1.屏蔽中断

在每个进程刚刚进入临界区后立即屏蔽所有中断,在离开之前再次打开中断,这样在CPU发生时钟中断或者其他中断时就不会释放CPU了。缺点:把中断的权利交给进程不安全

2.3.2.2. 锁变量

有一个共享锁变量,在进程进入临界区前先测试锁变量的值,为0则进入,为1则持续等待直到变为0。但这样做的缺点是如果一个进程在检测为0准备进入时CPU的执行权交给了另一个进程,此进程检测锁变量依然为0而后进入临界区;CPU执行权再次交给第一个进程时第一个进程也会直接进入临界区。

2.3.2.3.严格轮换法

这种方法适用于两个紧凑执行的进程,如果A从临界区出来将turn置为1而B迟迟不进入临界区,那么如果此时A想再次进入临界区就不可以进入,所以这种方法并不是一个好办法。

//两个循环分别控制两个进程 
/**进程A的控制 turn等于0时进程A进入临界区运行,出区时设置为1 */ 
while(TRUE){ 
while(turn!=0);
critical_region(); 
turn=1; 
noncritical_region(); 
} 
/**进程B的控制 turn等于1时进程B进入临界区运行,出区时设置为0 */ 
while(TRUE){ 
while(turn!=1); 
critical_region(); 
turn=0;
 noncritical_region();
 }

2.3.3 睡眠与唤醒

为了改善忙等待问题。将得不到资源时的忙等待,变为阻塞。

通信原语:

sleep:一个系统调用,作用是引起进程阻塞

wakeup(参数是要被唤醒的进程)

eg:解决生产者-消费者问题

问题描述:消费者和生产者两个进程共享一个缓冲区,缓冲区空的情况下,消费者阻塞,生产者进程活动;缓冲区满的情况下,生产者阻塞,消费者进程活动。与之前的绝对性互斥不同,此问题需要一个变量count来记录缓冲区的填充情况.

//生产者 
void producer(void){ 
int item;//产品 
while(TRUE){ 
item=produce_item(); 
if(count==N) sleep();//满则睡 
insert_item(item);//不满,放 
count=count+1;//数量+1 
if(count==1) wakeup(consumer);//放入之后数量为1,则之前数量一定是0,所以消费者进程一定在阻塞,唤醒之 
} 
} 

//消费者 
void consumer(void){ 
int item; 
while(TRUE){ 
if(count==0) sleep();//空则睡 
item=remove_item();
 count=count-1; 
if(count==N-1) wakeup(producer); 
consume_item(item); 
} 
}

因为未对count的访问加以限制,所以可能会出现以下情况:消费者读取到count为0,未来得及调用阻塞方法就交出了CPU,生产者获得CPU向缓冲区放置数据项,然后根据代码count为1,会执行一个唤醒消费者的操作,而此时消费者并未阻塞,所以wakeup信号失效;等消费者再次获得CPU,执行sleep(),但生产者后续不会执行wakeup()了,会导致缓冲区填满,两个进程睡眠。

解决方案:加唤醒等待位,当唤醒信号发出而对象醒着的时候,将该位置设为1.随后进程睡眠时,检测到该位置为1的话,就会发出唤醒信号。

2.3.4 信号量

为解决wakeup()丢失的问题,将测试信号量、更新信号量、以及发出睡眠信号的行为封装成一个原子操作。

两个操作:P(down) V(up)

三个信号量:full:记录充满的缓冲槽的数目 empty:空的缓冲槽数目 mutex:确保缓冲区互斥,只能有一个进程访问。

生产者流程

消费者流程

produce_item();

P(&empty);

P(&mutex);

insert_item();

V(&mutex);

V(full);

P(&full);

P(&mutex);

remove_item();

V(&mutex);

V(&empty);

 

2.3.5 消息传递

是一组系统调用:

send(destination,&message); //向指定目标发发送消息

receive(source,&message);//从指定源取出消息

消息传递有一个重传机制:接受方收到消息后要给予应答,若发送方没有收到应答则再次重发消息。如果是应答信号丢失导致重发消息的话,接受方会得到两条一模一样的消息,但是消息中有内嵌序号哦,如果检测到重复序号,则不予接收。

2.4 调度

2.4.1 为什么要研究调度算法

首先来看一下进行一次进程调度的过程:1)用户态切换到内核态 2)保存当前进程状态,比如在进程寄存器中的值,还有进程的内存映像 3)通过运行调度算法选定一个新进程,将此进程的内存映像重新装入MMU,最后新进程运行。

由上面的过程可以看到进行一次进程切换是比较麻烦的,频繁切换会浪费CPU时间,所以需要好的调度算法来进行切换.

何时进行调度:1)创建新进程之后,需要决定要运行的是子进程还是父进程 2)一个进程退出时

3)进程阻塞在I/O或者信号量上时 4)I/O中断完成时,完成了I/O的进程就进入了就绪状态,此时就要进行调度

2.4.2 批处理系统中的调度算法

批处理系统适用于处理周期性作业。对每个进程的响应时间要求不高,因此调度算法应该尽量减少进程切换来保证效率,所以用非抢占式算法或者对每个进程都有长时间周期的抢占式算法。

2.4.2.1 先来先服务

按照请求CPU的顺序进行调度。缺点显而易见,没有考虑进程之间相互协作相互联系。

2.4.2.2 短作业优先

按照每个作业的长短排序,最短的作业先运行。如果有大量短作业的话长作业将迟迟不能开始。

2.4.2.3 最短剩余时间优先

调度程序选择剩余运行时间最短的那个作业运行。是抢占式的调度算法,如果新进来的进程所需时间比正在运行的进程的所需时间更少,就挂起正在运行的进程。

2.4.3 交互式系统中的调度算法

2.4.3.1 时间片轮转

每个进程被分配一个时间片,到时间后剥夺CPU使用权给下一个进程。调度算法只需要维护一张可运行的进程表,就可以实现。但是时间片的长度设置是一个问题,太短进程切换频繁,CPU效率下降;太长导致后面的短作业长时间得不到响应。

2.4.3.2 优先级调度

为进程赋予一个优先级,调度时优先级高的进程先运行。为了防止优先级高的进程无休止运行,可以在每一次时钟中断的时候降低其优先级。

优先级也可以动态赋予,根据它在上一个时间片占用CPU的程度来决定其优先级;如果在50ms的时间片中只用了1ms,说明它可能是个I/O密集型进程,应拥有高优先级尽快完成在CPU上的工作去进行I/O。

2.4.3.3 高响应比调度

响应比=响应时间/要求服务时间

响应时间会随着等待时间一直增加,响应比也会不断提升,所以经过长时间等待的作业总会迎来执行时间

TIPS:

什么是中断?

中断的意思可以等同于请求,CPU接到中断请求之后,暂停执行当前任务,保护现场后进行中断处理中断分为三类:1)由CPU外部引起的,称作中断,如I/O中断、时钟中断、控制台中断等。2)是来自CPU的内部事件或程序执行中的事件引起的过程,称作异常,CPU本身故障、程序故障等。3)因执行系统调用而使用了TRAP指令

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读