进程
进程控制块
进程控制块(Processing Control Block),是操作系统核心中一种数据结构,主要表示进程状态。其作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位或与其它进程并发执行的进程。或者说,OS是根据PCB来对并发执行的进程进行控制和管理的。 PCB通常是系统内存占用区中的一个连续存区,它存放着操作系统用于描述进程情况及控制进程运行所需的全部信息,它使一个在多道程序环境下不能独立运行的程序成为一个能独立运行的基本单位或一个能与其他进程并发执行的进程。
进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构。系统用它来记录进程的外部特征,描述进程的运动变化过程。同时,系统可以利用PCB来控制和管理进程,所以说,PCB(进程控制块)是系统感知进程存在的唯一标志。
PCB内容
PCB通常记载进程之相关信息,包括
- 程序计数器:接着要运行的指令地址。
- 进程状态:可以是new、ready、running、waiting或 blocked等。
- CPU暂存器:如累加器、索引暂存器(Index register)、堆栈指针以及一般用途暂存器、状况代码等,主要用途在于中断时暂时存储数据,以便稍后继续利用;其数量及类因电脑架构有所差异。
- CPU排班法:优先级、排班队列等指针以及其他参数。
- 存储器管理:如标签页表等。
- 会计信息:如CPU与实际时间之使用数量、时限、账号、工作或进程号码。
- 输入输出状态:配置进程使用I/O设备,如磁带机。
或按照信息类型划分:
- 进程标识信息
进程标识信息用于唯一地标识一个进程,一个进程通常有两种标识符:内部标志符&外部标识符。
为了描述进程间的家族关系,通常还设有父进程标识和子进程标识,以表示进程间的家族关系。
此外,还设有用户名或用户标识号表示该进程属于哪个用户。- 内部标志符: 由操作系统赋予每个进程的一个唯一的数字标识符,它通常为一个进程的序号,方便了系统使用。
- 外部标识符: 由创建者产生,是由字母和数字组成的字符串,为用户进程访问该进程提供方便。
- 处理机状态
处理机状态信息主要由处理机的各个寄存器内的信息组成。 进程运行时的许多信息均存放在处理机的各种寄存器中。其中程序状态字(PSW)是相当重要的,处理机根据程序状态寄存器中的PSW来控制程序的运行。 - 进程调度信息
PCB中还存放着一些与进程调度有关的信息。- 进程状态。 标识进程的当前状态(就绪、运行、阻塞),作为进程调度的依据。
- 进程优先级。 表示进程获得处理机的优先程度。
- 为进程调度算法提供依据的其他信息。例如,进程等待时间、进程已经获得处理器的总时间和进程占用内存的时间等。
- 事件。 是指进程由某一状态转变为另一状态所等待发生的事件。(比如等待I/O释放)
- 进程控制信息
- 程序和数据地址。 是指组成进程的程序和数据所在内存或外存中的首地址,以便在调度该进程时能从其PCB中找到相应的程序和数据。
- 进程同步和通信机制。 指实现进程同步和通信时所采取的机制,如消息队列指针和信号量等,他们可以全部或部分存在PCB中。
- 资源清单。 列出了进程所需的全部资源 及 已经分配给该进程的资源,但不包括CPU.
- 链接指针。它给出了处于同一队列中的下一个PCB的首地址。
进程控制块PCB的组织方式
- 线性表方式:不论进程的状态如何,将所有的PCB连续地存放在内存的系统区。这种方式适用于系统中进程数目不多的情况。
- 索引表方式:该方式是线性表方式的改进,系统按照进程的状态分别建立就绪索引表、阻塞索引表等。
- 链接表方式:系统按照进程的状态将进程的PCB组成队列,从而形成就绪队列、阻塞队列、运行队列等。
进程上下文
进程是在操作系统支持下执行的,进程执行时需要操作系统为其设置相应的执行环境,如系统堆栈、地址映像寄存器、程序计数器、程序状态字、打开文件表以及相关通用寄存器等。 所以,把进程的物理实体与支持进程执行的物理环境合称为进程上下文。
- 上文: 把已执行的进程指令和数据在相关寄存器与堆栈中的内容称为上文。
- 正文: 把正在执行的进程指令和数据在相关寄存器与堆栈中的内容称为正文。
- 下文: 把待执行的进程指令和数据在相关寄存器与堆栈中的内容称为下文。
Unix System Ⅴ 的进程上下文组成:由用户级上下文、寄存器上下文、系统级上下文组成。
- 用户级上下文:由进程的用户程序段部分编译而成的用户正文段、用户数据和用户栈等组成。
- 寄存器上下文:由程序计数器(PC)、处理机状态字(PS)、栈指针和通用寄存器组成。 PC给出CPU将要执行的下一条指令的虚地址;PS给出机器与该进程相关联时的硬件状态;栈指针指向下一项的当前地址;通用寄存器则用于不同执行模式之间的参数传递。
- 系统级上下文又分为静态部分和动态部分。 这里的动态部分是指进入和退出不同的上下文层次时,系统为各层上下文中相关联的寄存器值所保存和恢复的记录。 系统级上下文静态部分包括PCB结构、将进程虚地址空间映射到物理空间的有关表格、核心栈等。 这里,核心栈主要用来装载进程中所使用的系统调用的调用序列。
系统级上下文的动态部分是与寄存器上下文相关联的。
进程上下文的层次概念主要体现在动态部分中,即系统级上下文的动态部分可看成是由一些数量变化的层次组成,其变化规则符合许先进后出的堆栈方式。
进程状态
三状态进程模型:
挂起:处在挂起状态的进程映像在磁盘上,目的是减少内存占用
具有挂起状态的进程模型:
进程切换
进程上下文切换发生在不同的进程之间而不是同一个进程内。
进城上下文切换分成三个步骤:
- 把被切换进程的相关信息保存到有关存储区,例如该进程的PCB中。
- 操作系统中的调度和资源分配程序执行,选取新的进程。
- 将被选中进程的原来保存的正文部分从有关存储区中取出,并送至寄存器与堆栈中,激活被选中进程执行。
总结: 进程上下文切换的切换过程涉及由谁来保护和获取进程的正文的问题,也就是如何使寄存器和堆栈等中的数据流入流出 PCB 的存储区。 另外,进程上下文切换还涉及系统调度和分配程序,这些都比较耗费CPU时间。
fork
由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。(进程id 0总是由交换进程使用,所以一个子进程的进程id不可能为0 )。
fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。
可以这样想象,2个进程一直同时运行,而且步调一致,在fork之后,他们分别作不同的工作,也就是分岔了。这也是fork为什么叫fork的原因
至于那一个最先运行,可能与操作系统(调度算法)有关,而且这个问题在实际应用中并不重要,如果需要父子进程协同,可以通过原语的办法解决。
进程通讯方式
管道/匿名管道(pipe)
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
- 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
- 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
- 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
管道的实质:
管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程则顺序的读出数据。
该缓冲区可以看做是一个循环队列,读和写的位置都是自动增长的,不能随意改变,一个数据只能被读一次,读出来以后在缓冲区就不复存在了。
当缓冲区读空或者写满时,有一定的规则控制相应的读进程或者写进程进入等待队列,当空的缓冲区有新数据写入或者满的缓冲区有数据读出来时,就唤醒等待队列中的进程继续读写。
管道的局限:
管道的主要局限性正体现在它的特点上:
- 只支持单向数据流;
- 只能用于具有亲缘关系的进程之间;
- 没有名字;
- 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);
- 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;
有名管道(FIFO)
匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO)。
有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。值的注意的是,有名管道严格遵循先进先出(first in first out),对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。有名管道的名字存在于文件系统中,内容存放在内存中。
匿名管道和有名管道总结:
- 管道是特殊类型的文件,在满足先入先出的原则条件下可以进行读写,但不能进行定位读写。
- 匿名管道是单向的,只能在有亲缘关系的进程间通信;有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
- 无名管道阻塞问题:无名管道无需显示打开,创建时直接返回文件描述符,在读写时需要确定对方的存在,否则将退出。如果当前进程向无名管道的一端写数据,必须确定另一端有某一进程。如果写入无名管道的数据超过其最大值,写操作将阻塞,如果管道中没有数据,读操作将阻塞,如果管道发现另一端断开,将自动退出。
- 有名管道阻塞问题:有名管道在打开时需要确实对方的存在,否则将阻塞。即以读方式打开某管道,在此之前必须一个进程以写方式打开管道,否则阻塞。此外,可以以读写(O_RDWR)模式打开有名管道,即当前进程读,当前进程写,不会阻塞。
信号(Signal)
- 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
- 如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程回复执行并传递给它为止。
- 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。
Linux系统中常用信号:
- SIGHUP:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。
- SIGINT:程序终止信号。程序运行过程中,按Ctrl+C键将产生该信号。
- SIGQUIT:程序退出信号。程序运行过程中,按Ctrl+\键将产生该信号。
- SIGBUS和SIGSEGV:进程访问非法地址。
- SIGFPE:运算中出现致命错误,如除零操作、数据溢出等。
- SIGKILL:用户终止进程执行信号。shell下执行kill -9发送该信号。
- SIGTERM:结束进程信号。shell下执行kill 进程pid发送该信号。
- SIGALRM:定时器信号。
- SIGCLD:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。
信号来源
信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:
- 硬件来源:用户按键输入Ctrl+C退出、硬件异常如无效的存储访问等。
- 软件终止:终止进程信号、其他进程调用kill函数、软件异常产生信号。
信号生命周期和处理流程
- 信号被某个进程产生,并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统;
- 操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号,而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号。
- 目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。
消息(Message)队列
- 消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
- 与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
- 另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。
消息队列特点总结:
- 消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识.
- 消息队列允许一个或多个进程向它写入与读取消息.
- 管道和消息队列的通信数据都是先进先出的原则。
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。
- 消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
- 目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,系统V消息队列目前被大量使用。系统V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。
共享内存(share memory)
- 使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
- 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
- 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。
%!(EXTRA markdown.ResourceType=, string=, string=)
信号量(semaphore)
信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。
为了获得共享资源,进程需要执行下列操作:
- 创建一个信号量:这要求调用者指定初始值,对于二值信号量来说,它通常是1,也可是0。
- 等待一个信号量:该操作会测试这个信号量的值,如果小于0,就阻塞。也称为P操作。
- 挂出一个信号量:该操作将信号量的值加1,也称为V操作。
为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。Linux环境中,有三种类型:Posix(可移植性操作系统接口)有名信号量(使用Posix IPC名字标识)、Posix基于内存的信号量(存放在共享内存区中)、System V信号量(在内核中维护)。这三种信号量都可用于进程间或线程间的同步。
信号量与普通整型变量的区别:
- 信号量是非负整型变量,除了初始化之外,它只能通过两个标准原子操作:wait(semap) , signal(semap) ; 来进行访问;
- 操作也被成为PV原语(P来源于荷兰语proberen”测试”,V来源于荷兰语verhogen”增加”,P表示通过的意思,V表示释放的意思),而普通整型变量则可以在任何语句块中被访问;
信号量与互斥量之间的区别:
- 互斥量用于线程的互斥,信号量用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源- 互斥量值只能为0/1,信号量值可以为非负整数。
也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。- 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
套接字(socket)
套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。
%!(EXTRA markdown.ResourceType=, string=, string=)
线程
用户线程
用户线程是完全建立在用户空间的线程库,用户线程的创建、调度、同步和销毁全又库函数在用户空间完成,不需要内核的帮助。因此这种线程是极其低消耗和高效的。
- 处理器竞争:单纯的用户线程是建立在用户空间,其对内核是透明的,因此其所属进程单独参与处理器的竞争,而进程的所有线程参与竞争该进程的资源。
- 使用资源:与所属进程共享进程地址空间和系统资源。
- 调度:由在用户空间实现的线程库,在所属进程内进行调度
LWP虽然本质上属于用户线程,但LWP线程库是建立在内核之上的,LWP的许多操作都要进行系统调用,因此效率不高。而这里的用户线程指的是完全建立在用户空间的线程库,用户线程的建立,同步,销毁,调度完全在用户空间完成,不需要内核的帮助。因此这种线程的操作是极其快速的且低消耗的。
上图是最初的一个用户线程模型,从中可以看出,进程中包含线程,用户线程在用户空间中实现,内核并没有直接对用户线程进程调度,内核的调度对象和传统进程一样,还是进程本身,内核并不知道用户线程的存在。
用户线程之间的调度由在用户空间实现的线程库实现。
这种模型对应着多对一线程模型,其缺点是一个用户线程如果阻塞在系统调用中,则整个进程都将会阻塞。
内核线程
内核线程就是内核的分身,一个分身可以处理一件特定事情。这在处理异步事件如异步IO时特别有用。内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads kernel )。
内核线程只运行在内核态,不受用户态上下文的拖累。
- 处理器竞争:可以在全系统范围内竞争处理器资源;
- 使用资源:唯一使用的资源是内核栈和上下文切换时保持寄存器的空间
- 调度:调度的开销可能和进程自身差不多昂贵
- 同步效率:资源的同步和数据共享比整个进程的数据同步和共享要低一些。
轻量级进程LWP
轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。
轻量级进程由clone()系统调用创建,参数是CLONE_VM,即与父进程是共享进程地址空间和系统资源。
与普通进程区别:LWP只有一个最小的执行上下文和调度程序所需的统计信息。
- 处理器竞争:因与特定内核线程关联,因此可以在全系统范围内竞争处理器资源
- 使用资源:与父进程共享进程地址空间
- 调度:像普通进程一样调度
轻量级线程(LWP)是一种由内核支持的用户线程。它是基于内核线程的高级抽象,因此只有先支持内核线程,才能有LWP。每一个进程有一个或多个LWPs,每个LWP由一个内核线程支持。这种模型实际上就是一对一线程模型。在这种实现的操作系统中,LWP就是用户线程。
由于每个LWP都与一个特定的内核线程关联,因此每个LWP都是一个独立的线程调度单元。即使有一个LWP在系统调用中阻塞,也不会影响整个进程的执行。
轻量级进程具有局限性。
- 首先,大多数LWP的操作,如建立、析构以及同步,都需要进行系统调用。系统调用的代价相对较高:需要在user mode和kernel mode中切换。
- 其次,每个LWP都需要有一个内核线程支持,因此LWP要消耗内核资源(内核线程的栈空间)。因此一个系统不能支持大量的LWP。
线程和进程区别
我的理解是进程是指在系统中正在运行的一个应用程序;程序一旦运行就是进程,或者更专业化来说:进程是指程序执行时的一个实例。
线程是进程的一个实体。
进程——资源分配的最小单位,线程——程序执行的最小单位。
- 因为进程拥有独立的堆栈空间和数据段,所以每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这对于多进程来说十分“奢侈”,系统开销比较大,而线程不一样,线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高,但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。
- 体现在通信机制上面,正因为进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便。
- 属于同一个进程的所有线程共享该进程的所有资源,包括文件描述符。而不同过的进程相互独立。
- 线程又称为轻量级进程,进程有进程控制块,线程有线程控制块;
- 线程必定也只能属于一个进程,而进程可以拥有多个线程而且至少拥有一个线程;
- 体现在程序结构上,举一个简明易懂的列子:当我们使用进程的时候,我们不自主的使用if else嵌套来判断pid,使得程序结构繁琐,但是当我们使用线程的时候,基本上可以甩掉它,当然程序内部执行功能单元需要使用的时候还是要使用,所以线程对程序结构的改善有很大帮助。