全面总结:进程与线程

进程三态状态装换图

 

就绪态只需要等待处理机(CPU)

阻塞态可能在等待输入输出,即使分配给处理机也是徒劳

调度进程,只需要等待就绪队列里的进程,因为阻塞状态可以转换到就绪队列里去

就绪原因:

    缺少cpu

阻塞原因

 1.内存资源紧张

2.  无就绪队列,处理机空闲

3 .I/O速度比处理机速度慢的多,可能出现全部进程阻塞等待I/O。

解决方法

l  交换技术:换出一部分暂时不能运行的进程(阻塞进程)到外存(只换出程序和数据,PCB不换出去),以腾出内存空间,可以调用新进程来执行。

l  虚拟存储技术:每个进程只能装入一部分程序和数据

挂起原因:

       挂起:进程被交换到外存,状态变为挂起状态

 进程挂起的原因

1.进程全部阻塞,处理机空闲。

2.系统负荷过重,内存空间紧张。(让其他进程先执行完)

3.操作系统的需要。操作系统可能需要挂起后台进程或一些服务进程,或者某些可能导致故障的进程。

 4.终端用户请求。

5父进程的需求。

 挂起进程的特征

l  不能立即执行

l  可能是等待某事件发生,若是,则阻塞条件独立于挂起条件,即使阻塞事件发生,该进程也不能执行。

n  阻塞和挂起没有联系。

n  如果A进程现在要求输入数据,此时A进程属于阻塞状态,在选择挂起进程的时候,可能先选择阻塞进程(A进程),此时A进程挂起,正在输入数据,输入到内存缓冲区内。当数据输完了,向处理机发送命令数据已经输入完成,阻塞事件解除,但实际上还是挂起,所以仍是挂起态。

l  使之挂起的进程为:自身、父进程、OS。

l  只有挂起它的进程才能使之由挂起状态转换为其他状态。

  阻塞与挂起

l  进程是否等待时间:阻塞与否。

l  进程是否被换出内存:挂起与否。

4.4  四种状态组合

l  就绪:进程在内存,准备执行。

l  阻塞:进程在内存,等待事件。

l  就绪/挂起:进程在外存,只要调入内存即可执行。

l  阻塞/挂起:进程在外存,等待事件。

  处理机可调度执行的进程有两种

l  新创建的进程

l  或换入一个以前挂起的进程

通常为避免增加系统负载,系统会换入一个以前挂起的进程执行。

4.6  具有挂起状态的进程状态转换

l  阻塞 → 阻塞/挂起:OS通常将阻塞进程换出,以腾出内存空间

l  阻塞/挂起→ 就绪/挂起:当阻塞/挂起进程等待的事件发生时,可以将其转换为就绪/挂起。

l  就绪/挂起→ 就绪:OS需要调入一个进程执行。

l  就绪 → 就绪/挂起:一般,OS挂起阻塞进程。但是有时也会挂起就绪进程,释放足够的内存空间。

l  新 → 就绪/挂起(新→ 就绪):新进程创建后,可以插入到就绪队列或就绪,挂起队列,若无足够的内存分配给新进程,则需要新→ 就绪/挂起。

----------------------------------------------------------------------------------------------------------------------------------------------------------

所以,我们必须知道,进程为何挂起?因为内存空间资源紧张,所以要把阻塞或者就绪的进程从内存切换至外存,释放内存空间;所以说阻塞或者就绪的进程是在内存中的,只是没有占用CPU的资源;

还有一个问题就是,我们知道,进程一般是自己阻塞自己,等待某个时间的发生;当某个事件发生时,进程只能由其他进程或者OS唤醒,这是为什么?显然,进程阻塞它就停止运行,释放处理机资源了,就是说它自己和睡着了差不多,你怎么让一个睡着的人自己叫醒自己呢?很好理解。

----------------------------------------------------------------------------------------------------------------------------------------------------------

接下来,我们来看一下进程的创建、执行、等待、终止过程

具体的流程如下。

我们知道,操作系统通过fork()调用来创建子进程。系统最初的初始进程(或者称为root进程)为init进程。

当我们创建子进程之后涉及到父子进程之间的进程地址空间的复制,一般来说,我们会在子进程创建的时候将父进程的地址空间包括地址空间里的代码、数据和堆栈等等资源全部拷贝一份给子进程,添加修改页表项。当子进程在通过exec()系统调用执行的时候,会去打开新的代码文件,将原始的父进程拷贝过来的内容覆盖掉,执行新的内容。这就造成一个问题,什么问题?既然子进程对于父进程的内容没有执行意义,而且拷贝会占用系统资源,那何必要进行创建时的拷贝工作呢?

所以,我们引入一种COPY ON WRITE 技术(写时覆盖),就是在创建的时候仅仅修改页表项,拷贝所需的原始数据资源。当我们的子进程在执行的时候需要进行写操作的时候,才进行相应的拷贝和覆盖。因为如果子进程仅仅只是读取操作的话,完全没有拷贝的需要,直接读取父进程地址空间的内容即可。

当进程调用exec()的时候不一定是立刻运行的,理论上是进行running。但是由于会进行磁盘文件数据的换入操作,所以当前进程还是会阻塞,等待换入完成则就绪---》执行。

进程的等待:当子进程通过exit()退出的时候,系统会释放掉其用户空间的资源,但是其存在于内核中的资源(例如PCB)却没有回收(因为系统内核通过进程的PCB来对相应的进程做出处理,PCB存在于内核空间的相应的进程执行队列)。

那么怎么办呢?那肯定是通过其父进程来进行回收。因此父进程在其子进程通过exit()退出后,通过调用wait()来得到子进程的终止状态信息,然后进行子进程PCB等资源的相应回收处理。

在子进程调用exit()到父进程调用wait()之间的状态,称作“”僵尸状态“”。

还有一种情况就是,当子进程终止时,其父进程也终止了,那么由于系统的init进程会定期扫描系统的进程表来筛选出僵尸进程,并做相应的回收处理。

考虑当父进程终止,但其子进程还未终止的情况。此时子进程称为“”孤儿进程“”,由init进程收养。

-------------------------------------------------------------------进程同步互斥--------------------------------------------------------------------

   补充一个坑爹坑爹坑爹坑爹的问题:系统如何将一个信号通知到进程?

     前面讲过信号了,为什么又问一遍?这个问题有什么特殊的地方么? 
     内核给进程发送信号,是在进程所在的进程表项的信号域设置对应的信号的位。

        更多:http://blog.csdn.net/joejames/article/details/37960873 

-------------------------------------------------------------------线程同步互斥--------------------------------------------------------------------

  linux系统的通讯机制,主要是指进程间通讯,其实通讯就是进程同步的手段。

      这里要说的linux系统的同步机制是讲线程间的同步。 


  互斥量 
  首先是最基础的加锁原语,互斥量。既确保同一时间只有一个线程访问数据,通过在访问共享资源前对互斥量加锁,阻塞其他试图再次加锁的线程知道互斥锁被释放。互斥的具体实现有多种方法,例如开关中断,使用原子的机器指令。 
  读写锁 
  与互斥量类似,不过允许更高的并行性。读写锁有三种状态,读模式的加锁,写模式的加锁,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是可以多个线程可以同时占用读模式的读写锁。既读模式下可以共享,写模式下互斥。一般一个线程试图以读模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。 
  条件变量 
  互斥量是加锁原语,条件变量属于等待原语,用于等待某个条件成立后唤醒阻塞的线程。条件变量与互斥量一起使用,条件本身由互斥量保护。Java Object内置了条件变量wait(),notify(),notifyAll()。    
  pthread_cond_wait(),pthread_cond_signal(),pthread_cond_broadcast(Unix),从函数的命名就可以看出其大致作用。 
  根据陈硕的总结,条件变量的正确使用方式: 
  对于wait端: 
  1.必须与mutex一起使用。 
  2.在mutex已上锁时才能调用wait()。 
  3.把判断布尔条件和wait()放到while循环中。 
  第三个条件主要是为了防止spurious wakeup,既虚假唤醒。因为pthread_conf_wait能被除了pthread_cond_signal(),pthread_cond_broadcast外的其他信号唤醒。需要再wait后再次检查,同时也是为了避免错过一次条件变量后永远的等待下去。 
  对于signal端: 
  1.一定不要在mutex已经上锁的情况下调用signal。 
  2.在signal之前一般要修改布尔表达式。 
  3.修改布尔表达式通常用mutex保护。 
  4.注意区分signal和broadcast:“broadcast通常用于表明状态变化,signal通常用于表示资源可用”。 
  自旋锁 
  自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,二是在获取锁之前一直处于忙等。既一直占用CPU资源直到锁被释放。 
  屏障 
  屏障主要用于多个线程之间的并行工作的协调。屏障允许每个线程等待,直到所有的合作线程都达到某个点,然后从该点继续执行。 
  信号量 
  这个在《unix环境高级编程》中没有提及,在《操作系统》中有论述。 
  信号量可作用与进程间合作,以及多线程的同步。 
  一个进程可以被迫在某一个位置停止,直到接收到某一个信号。为了发信号,需要使用一个称为信号量的特殊变量,可以看做一个具有整数值得变量。其中只允许信号量取0和1的称为二元信号量。非二元信号量常称为计数信号量或一般信号量。 
  一般在信号量上定义三个操作: 
  1.一个信号量可以初始化成非负数。 
  2.semWait操作使信号量减1。如果值变为负数,则执行semWait的进程或线程被阻塞,否则继续执行。 
  3.semSignal操作使信号量加1。如果值<=0,则被semWait阻塞的进程被解除阻塞。 
  信号量需要队列保存阻塞在信号量上等待的进程。至于进程按什么顺序移除,最公平的是先进先出,采用此策略的为强信号量。没有规定顺序的为弱信号量。 
  互斥量和二元信号量的主要区别在于互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。至于用于互斥和用于同步的说法,十分牵强。 
  陈硕关于信号量的建议是不用。 
  因为可用条件变量加互斥量完全代替,另外还需要担心计数值需要和自己的数据长度常常保持一致的问题。 
  死锁 
  死锁大概已经被讲烂了,我也不想再搬运了。坚持使用Scoped Locking,死锁的时候好检测。 

---------------------------------------------------------------多线程和多进程的区别---------------------------------------------------------------------

多线程和多进程的区别(重点 必须从cpu调度,上下文切换,数据共享,多核cup利用率,资源占用,等等各方面回答,然后有一个问题必须会被问到:哪些东西是一个线程私有的?答案中必须包含寄存器,否则悲催)

  区别的意思是优缺点吧。 
   
  多线程:

  • 高效的内存共享,数据共享
  • 较轻的上下文切换开销,不用切换地址空间,不用更改CR3寄存器,不用清空TLB。
  • 创建销毁切换比较简单

    多进程:

  • 更强的容错性,不会一阻全阻,一个进程崩溃不会整个系统崩溃。

  • 更好的多核伸缩性,进程的使用将许多内核资源(如地址空间,页表,打开的文件)隔离,在多核系统上的可伸缩性强于多线程程序

    在多核利用率上,多进程和多线程同样可以提高多核利用率。 
    其实对于创建和销毁,上下文切换,其实在Linux系统下差别不大,Window下有较大差别。 
    综上,多进程和多线程的最主要的区别就在资源共享,隔离问题。如果工作使用的内存较大,使用多线程可以避免CPU cache的换入换出,影响性能。

    线程私有

  • ID,每个线程都有自己的ID作为进程中唯一的表述。

  • 一组寄存器值,用来保存状态
  • 各自的堆栈
  • 错误返回码,防止错误还未被处理就被其他线程修改。
  • 信号屏蔽码,每个线程感兴趣的信号不同。
  • 优先级
  • 共享的:进程的代码段,公有数据,进程打开的文件描述符,全局内存,进程的栈,堆内存等。

猜你喜欢

转载自blog.csdn.net/Fly_as_tadpole/article/details/82080002