【操作系统】2.进程和线程

  • 【操作系统】2.进程和线程已关闭评论
  • 77 次浏览
  • A+
所属分类:linux技术
摘要

操作系统main函数中最后 if(!fork()) {init();} ,也就是main函数最后创建了第1个进程,init执行了shell(Windows)桌面。


1.操作系统的多进程图像

操作系统main函数中最后 if(!fork()) {init();} ,也就是main函数最后创建了第1个进程,init执行了shell(Windows)桌面。

【操作系统】2.进程和线程

操作系统管理和组织进程都使用PCB(Process Control Block),不同的程序的PCB放在不同的位置,用于记录该进程运行时的状态。操作系统对进程进行分类,例如等待执行的进程和等待某些事件完成的进程,例如等待磁盘读写。

【操作系统】2.进程和线程
【操作系统】2.进程和线程
【操作系统】2.进程和线程
 
  1. 新建态:系统完成创建进程的一系列工作。只能转换到就绪态
  2. 就绪态:拥有除CPU之外的其他所需的所有资源。当拥有CPU时就可以转换到运行态
  3. 运行态:用于CPU和所需的所有资源
    1. 当时间片到或者处理机被抢占了,就转换到就绪态;
    2. 当进程用“系统调用”的方式申请某种系统资源或者请求等待某个事件的发生,则进入阻塞态(主动)
  1. 阻塞态:没有所需要的资源。当所需要的资源得到分配时,进入就绪态(被动)
  2. 终止态:进程运行结束或者出现不可修复的错误时,由运行态转到终止态

【操作系统】2.进程和线程

进程切换的三个部分:队列操作+调度+切换

pCur.state = 'W';    // 启动磁盘读写,将当前进程设置为阻塞状态 schedule();         // 将pCur放到DiskWaitQueue  schedule() {     pNew = getNext(ReadyQueue);  // 从就绪队列找到下一个进程,调度函数算法非常复杂     switch_to(pCur,pNew);  // 保存当前进程的现场,把下一个进程的现场恢复 }

把当前进程的现场保存到pCur中(PCB),把切换程序的pNew(PCB)读取到寄存器中

【操作系统】2.进程和线程

多个进程同时存在于内存的问题:不同进程的地址可能影响其他进程的代码,这可能导致其他进程的崩溃。操作系统需要维护一张映射表,将内存映射到实际的内存地址中,把不同的进程隔离开来保证进程的安全,下图中同样对内存100的操作分别映射到了内存地址780和内存地址1260。

【操作系统】2.进程和线程
但实际上多进程之间可能存在合作关系,比如打印机进程需要读取word进程的内容来完成打印的工作,这时可以提交到共享缓冲区。但这里可能存在一个问题,因为进程1和进程2是交替进行的,可能进程1首先读取到空间7是空的,接下来切换到进程2也读取到空间7是空的,开始向空间7写入,接下来切换到进程1继续在这里写入,会导致写入缓冲区的内容是错误的。所以操作系统需要管理一个合理的进程推进顺序。

【操作系统】2.进程和线程

2.用户级线程

进程 = 资源(映射表) + 指令执行序列

线程是只切换指令,如PC和寄存器,而不切换映射表,这种切换保留了并发了优点,避免了进程切换的代价

【操作系统】2.进程和线程

举例说明,对于浏览器来说,可以用一个线程接收服务器数据,一个线程显示文本,一个线程处理图片,一个线程显示图片,它们不需要用多个映射表完全分离开,没有必要用多个进程完成这些工作。我们需要的工作主要就是下面看到的两个部分,创建Create线程进行工作处理,使用Yield跳转到另一个线程工作。

void WebExplorer(){     char URL[] = "http://cms.hit.edu.cn";     char buffer[1000];     pthread_create(..., GetData, URL, buffer);     pthread_create(..., Show, buffer);  }  void GetData(char *URL, char *p) {...} void Show(char *p) {...};

【操作系统】2.进程和线程

 线程切换的详细过程:每个线程都有自己的栈。线程1执行过程中,首先调用函数B(),保护现场,将上一段程序的帧指针和函数B完成后PC应指向的地址压入栈(参见【深入理解计算机系统】3.程序的机器级表示),接下来调用Yield()函数,保护现场,将之前的帧指针和Yield函数结束后的PC204压入栈,接下来Yield函数将当前栈指针1000保存在TCB1中,并将栈指针切换到TCB2的栈指针2000,完成了线程间的切换。接下来线程2的Yield使得栈指针回到1000处,继续上一个线程对应位置执行。下面给出了用户级线程的Create和Yield核心代码。

【操作系统】2.进程和线程

void Yield(){     TCB1.esp = esp;  //Thread Control Block     esp = TCB2.esp; } void ThreadCreate(A){   TCB *tcb=malloc();   //申请空间保存TCB   *stack=malloc();    //申请空间保存栈   *stack = A;       //向栈中压入数据   tcb.esp=stack;     //将栈和TCB建立联系 }

【操作系统】2.进程和线程

3.内核级线程

用户级线程存在的问题,用户级线程在请求下载数据的过程中,理想情况是下载了一些后跳转到显示文本的线程执行,但实际上内核级线程不知道这些事情,由于等待网卡IO会阻塞这个进程,最后导致浏览器没有实现我们需要的功能。

【操作系统】2.进程和线程

所以引入内核级线程,ThreadCreate是系统调用,会进入内核,Yield的调度由系统决定。

【操作系统】2.进程和线程

接下来看一下多核和多CPU,可以看到多核CPU只有一套MMU(内存映射),也就是多核心CPU在执行进程的时候,也需要切换内存映射再执行,只有多处理器才能并行运行多个进程。但这个时候内核级线程的优势就体现出来了,多核CPU可以并行的执行同一进程不同线程的代码,因为这些代码共用一套内存映射。

【操作系统】2.进程和线程

对于内核级线程,它与用户级线程的区别是

用户级线程在用户栈执行,多个用户级线程对应了多个用户栈,1个TCB(内核态)关联1个用户栈;

内核级线程在用户栈和内核栈都需要执行和调用函数,所以多内核级线程实际上对应了多套栈(包括用户栈和内核栈),1个TCB(内核态)关联1个用户栈和1个内核栈。

【操作系统】2.进程和线程

int中断指令会引起内核栈的切换,内核栈中记录了用户栈用户代码两部分内容。SS寄存器(栈顶段地址)和SP寄存器(偏移地址)的值,SS:SP是此时栈顶位置;PC记录了用户代码程序运行的代码位置,CS记录了用户代码段基址

【操作系统】2.进程和线程

【操作系统】2.进程和线程

 内核级线程的切换包含5个阶段

1.中断入口(进入切换):系统中断线程1从用户态进入内核态,用户态寄存器的值保存到内核栈

2.中断处理(引发切换):调用schedule函数,引起TCB切换。这里有可能启动磁盘读写或时钟中断,内核会调用schedule找到下一个要执行的TCB,然后用next指针指向这个TCB

3.内核栈切换(switch_to):把当前ESP寄存器放在current指向的TCB中,然后把next指向的esp赋给寄存器,完成内核栈指向地址的切换,现在ESP指向了下一个线程的TCB地址

4.中断返回(iret):把TCB存储的内核栈现场恢复出来

5.用户栈切换:切换回用户态PC指针还有对应的用户栈

【操作系统】2.进程和线程

4.内核级线程实现

首先从这段代码开始,main函数开始,首先遇到函数A,用户栈中压入A的返回地址(也就是B的初始地址),在A函数执行中遇到fork()函数,首先将系统调用号__NR_fork移入%eax寄存器,然后调用INT 0x80中断,执行这条指令时PC自动加1,此时PC指向下一行mov res,%eax。触发INT 0x80中断后,cpu立刻找到用户栈对应的内核栈,将当前时刻的SS和SP压入内核栈,接下来将返回地址CSIP压入内核栈,也就是mov res,%eax这一行。接下来执行system_call

【操作系统】2.进程和线程

_system_call:     cmpl $nr_system_calls-1,%eax # 调用号超出范围就在eax设置-1并退出     ja bad_sys_call     push %ds %es %fs   # 保存原段寄存器值     pushl %edx %ecx %ebx    # 一个系统调用最多带3个参数,这里存放了系统对应C语言函数调用的参数     movl $0x10,%edx    # 设置ds和es到内核段     mov %dx,%ds     mov %dx,%ex    #edx的低16位赋值给ds和es指向内核数据段     movl $0x17,%edx     mov %dx,%fs     call _sys_call_table(,%eax,4)     pushl %eax  #系统调用返回值压入栈

下图为切换5段论的中断入口和中断出口。_system_call首先保护现场,将原段寄存器的值压入栈,然后将调用的参数压入栈,接下来调用sys_fork,他首先判断判断当前程序TCB是不是等于0,等于0说明已经就绪,如果不等于0说明线程阻塞,则应该重新调度reschedule(也就是切换5段论中间3段,切换TCB),完成后进行中断返回ret_from_sys_call

【操作系统】2.进程和线程

下图为切换5段论的中断出口,对应入口的大量push压入栈,出口把保存在TCB中的数据pop出栈

【操作系统】2.进程和线程

切换5段论中的switch_to使用的时TSS切换,是一个长跳转。TR表示当前cpu对应的任务段,TR改变时会把寄存器中的内容全部保存到旧的TSS中,然后把新的TSS中所有内容都会加载到寄存器

【操作系统】2.进程和线程

创建一个线程最重要的就是做出可以切换的样子。_sys_fork首先拷贝父进程的所有参数,这些参数都已经在中断过程压入内核栈,

【操作系统】2.进程和线程

copy_process的细节:创建栈。申请一页内存用于保存PCB和内核栈,注意这里内核栈重新创建,但ss和esp的栈与父进程一模一样,也就是它可以和父进程用同样的代码同样的栈,eip是int 0x80中断的下一句话。最后如果创建了子进程,会把%eax置为0,所以从子进程返回到mov res,%eax的时候,res是0;但如果从父进程返回到mov res,%eax,res是非0,所以有一段经典代码if(!fork()){子进程代码段}else{父进程代码段},这样就实现了子进程和父进程都返回这个位置,但执行不同的代码

【操作系统】2.进程和线程

【操作系统】2.进程和线程

如何让子进程执行我们想要的代码?下面给出了更为详细的代码,如果非fork则执行代码,如果是父进程则执行另一部分代码。

【操作系统】2.进程和线程

【操作系统】2.进程和线程

【操作系统】2.进程和线程

5.CPU调度策略

吞吐量和响应时间之间有矛盾:响应时间小 -> 切换次数多 -> 系统内耗大 -> 吞吐量小

前台任务和后台任务的关注点不同:前台任务关注响应时间(从提交到相应的时间间隔),后台任务关注周转时间(从提交到完成的时间间隔)

需要综合考虑IO约束型任务和CPU约束型任务

【操作系统】2.进程和线程

 应该综合考虑花费时间短的程序优先执行来降低周转时间,划分时间片来降低响应时间,同时也应该为前台和后台应用划分优先级

6.进程同步与信号量

不同进程需要合作,例如打印机的打印队列与word文档之间的合作,这种同步是通过信号量控制的

进程同步就是控制进程交替执行的过程,保证多进程合作合理有序

假设有3个生产者进程P,1个消费者进程C,1个缓冲区,用信号量来表示缓冲区的状态,这些进程就可以通过信号量实现进程同步(也就是进程的等待和唤醒)

(1)缓冲区满,P1执行,P1发现缓冲区满所以sleep,设置sem=-1(有1个进程等待,缓冲区缺少1个位置)

(2)P2执行,P2 sleep,设置sem=-2(有2个进程等待)

(3)C执行,打印1份文件,缓冲区增加1个空间,wakeup P1,设置sem=-1

(4)C再执行,缓冲区又增加1个空间,wakeup P2,设置sem=0

(4)C再执行,不需要唤醒进程,设置sem=1(缓冲区盈余1个位置)

(5)P3执行,因为缓冲区还有内容,直接执行,设置sem=0

信号量的临界区保护

信号量是一个共有的变量,大家一起修改一起使用,多进程切换过程中可能存在问题。下面生产者P1和P2会修改empty信号量,调用生产者P1或P2时,他们都会首先读取现在的信号量,接下来将信号量-1,并把这个值赋回给公共的信号量。接下来右图给出了一种可能的调度,由于生产者P1在信号量-1之后没有将该信号量赋值给公共的信号量,此时发生调度转到了生产者P2,这就导致本来应该两个生产者使信号量-2,但实际上只-1

【操作系统】2.进程和线程

解决方法:写共享变量empty时阻止其他进程访问,即上锁的思想

临界区:一次只允许一个进程进入的该进程的那一段代码,在这里就是每个进程中修改empty的这段代码,这里最重要的工作就是找到进程临界区的代码。核心思想就是进程进入临界区代码时进行一些操作,退出临界区后再进行一些操作,基本原则互斥进入,其次应该有空让进,并且是有限等待的。

下面是两种临界区控制的尝试,分别为轮换法和标记法。

轮换法: 使用turn变量控制进入。首先看互斥进入,如果P0进入说明turn=0,如果P1进入说明turn=1,满足互斥性,但是可能P0完成后将turn置为1,P1进程又在阻塞状态,就导致P1进程不使用临界区代码,P0进程又无法进入临界区代码,不满足有空让进

标记法:如果进程想要进入自己的临界区,就将自己的标记flag设置为true。首先看互斥性,如果P0进入说明flag[0]=true,flag[1]=false,如果P1进入说明flag[1]=true,flag[0]=false,满足互斥性。接下来看有空让进,两个进程都会检测对方是否想要进入临界区,如果想要进入就谦让,但有可能双方同时调整了自己的标志位,最后导致双方互相谦让,没有人能进入临界区

这两种标志太对称了,你也一样我也一样,最后卡死在这个地方

【操作系统】2.进程和线程

 Peterson算法:如果进程想要进入

【操作系统】2.进程和线程