你的收获:为什么会有进程和线程?什么是进程?进程的生老病死?什么是线程?线程如何避免共享资源的冲突以及线程实现方式?CPU根据进程的系统优先级如何进行上下文切换?如何进程不同需求场景下的内存需求?CPU如何调度程序的?
了解为什么会有进程和线程,可以更好的帮助你了解进程存在的意义。程序在运算的时候,会一直霸占着CPU资源造成资源浪费,有可能某个时间在写磁盘数据、读取网络设备数据,这时候完全可以把CPU的计算资源让给其他程序,直到数据读写准备就绪后再切换回来。CPU做逻辑运算的每条指令是从内存中读取的,但内存的大小是有限的,很可能装不下磁盘中的整个程序,内存中只允许运行当下需要的部分程序,等运算完继续读取后面一部分程序到内存并继续进行运算。怎么控制和管理多个程序间的计算机资源呢?划分资源管理的最小单元啊:进程。有了进程,操作系统才好在内存中加载程序的执行指令,方便调度程序在有限资源情况下更有效的利用资源,提高资源利用率。
即使划分了资源管理的最小单元,但是一个进程在运行的过程中,也不可能一直占据着CPU进行逻辑运算,运行过程中很可能在进行磁盘I/O或者网络I/O,资源还是有些浪费。为了更加充分利用CPU运算资源,提高资源利用率,更小的资源管理单元:线程应运而生。线程最大的特点是和创建它的进程共享地址空间。
进程是在处理器上执行的一个实例,在Linux操作系统上运行的所有进程都是通过进程描述符(task_struct结构)来管理的。一个进程描述符包含了单个进程在运行期间所有必要的信息,比如进程标识、进程的属性、构建进程的资源等。
每个进程也有自己的生老病死,也就是进程的生命周期,比如创建、执行、终止、删除等。
当父进程创建一个新的子进程时,父进程发出一个fork()系统调用,然后得到一个新创建的子进程的进程描述符,并设置一个新的进程ID。复制父进程的进程描述符的值给子进程,因此子进程和父进程有相同的地址空间(短暂的)。
exec()系统调用将新的程序复制到子进程的地址空间。因为父子进程共享相同的地址空间,所以新程序写数据时会导致页错误。怎么解决呢?内核会给子进程分配新的物理页。这种延迟的操作被称为Copy On Write。通常子进程执行它自己的程序,而不是执行与父进程相同的操作。此操作可避免不必要的开销,因为复制整个地址空间是非常慢和低效的操作,会占用处理器大量的时间与资源。
当程序执行完成,系统调用exit()来终止子进程。exit()系统调用释放进程的大部分数据结构并发送一个终止信号通知父进程。此时的进程被称为僵尸进程(zombie process)。
子进程不会完全被移除,直到父进程通过wait()系统调用得知子进程已终止。只要子进程的终止通知发送到父进程,父进程就会移除所有子进程的数据结构,并释放进程描述符。
线程是在进程中产生的一个执行单元,在同一个进程中与其他线程并行运行。线程们共享相同的资源,比如内存、地址空间、打开的文件等。因为共享资源,所以每个线程不能同时改变共享的资源,因此互斥、锁、序列化等是用户应用程序要实现的机制。
从性能来看,线程的创建要比进程的创建开销更小,因为创建线程不需要复制资源(比如子进程创建要从父进程复制资源)。另一方面,进程和线程在调度算法上有相似的特征。内核处理它们使用相似的方式。
在当前的Linux 实现中,线程支持POSIX(可移植操作系统接口)UNIX兼容库(pthread)。在Linux操作系统中有如下3种实现:
1) LinuxThreads:从Linux2.0内核开始,成为默认的线程实现。LinuxThreads有一些不符合POSIX 标准的实现。未来的企业级Linux发行版不支持LinuxThreads。
2) Native POSIX Thread Library(NPTL):NPTL最初由Red Hat开发的,NPTL更加符合POSIX标准,增强2.6内核的性能,比如新的clone()系统调用、信号处理的实现等。它比LinuxThreads有更好的性能和可扩展性。
NPTL与 LinuxThreads有一些不兼容。如果一个应用程序依赖于LinuxThread,则通过NPTL实现可能不能工作。
3) Next Generation POSIX Thread(NGPT 下一代POSIX线程):NGPT是IBM开发的POSIX线程库的版本。它当前正处在维护状态,并没有进一步发展的计划。
进程优先级是针对CPU资源来定义的。CPU资源是有限的,进程数目是不固定的,进程的重要性是不同的,前端的进程如果优先级不高,客户端界面没及时响应(2-5-8原则),产品早没人用了。
如果你是CPU,怎么知道各个进程的重要性,根据重要性来给进程分配资源呢?如果有这么一个进程重要性有关的表格,可以根据重要性列个系统优先级(system priority),从高到低为0-139。
最高静态(实时)优先级(99)对应于系统优先级0,最低静态(实时)优先级(0)对应于系统优先级99。这些静态(实时)优先级,系统是不能动态改变它们的。
对于动态(非实时)优先级,内核需要使用一个基于进程行为和特征的算法做上下+/-5的动态调整。一个进程可以间接地使用进程的nice级别来改变动态优先级。Linux支持的nice级别从高到低为-20到19,默认值是0。
上面介绍了CPU根据进程的系统优先级或系统中断(下面介绍)来调度进程,这就涉及到进程的切换。
在CPU执行期间,运行进程的信息被存储在CPU的寄存器和高速缓存(cache)中,执行的进程被加载到寄存器的数据集被称为上下文(context)。在切换过程中,先存储运行进程的上下文,然后将下一个待运行进程的上下文恢复到寄存器。进程描述符和内核模式堆栈区域用于存储上下文,这个切换的过程被称为上下文切换(context switching)。一般不能有太多的上下文切换,因为CPU每次要刷新寄存器和高速缓存(cache),以便释放空间给新的进程,这可能会导致性能问题。
中断处理是优先级最高的任务之一。中断通常由I/O设备产生,比如网络接口卡、键盘、磁盘控制器、串行适配卡等。中断处理是Linux内核通知事件(比如键盘输入、以太网帧到达等)。它告诉内核中断进程执行,并要尽可能快地执行中断处理,因为有些设备需要快速响应。这对于系统的稳定性是至关重要的。当一个中断信号到达内核的时候,内核必须从当前执行的进程切换到一个新的进程,以处理这个中断。这意味着中断会导致上下文切换,也暗示大量的中断会导致性能下降。
在Linux实现中有两种类型的中断。硬中断是由硬件设备产生的,需要快速响应(如磁盘 I/O、网络适配器、键盘、鼠标等),可以在/proc/interrupts下看到硬件中断相关的信息。软中断被用来处理可以推迟的任务(如TCP/IP 操作、SCSI协议操作等)。
在一个多处理器的环境中,中断由每个处理器处理的。将中断绑定到单个处理器上可以提高系统的性能。
TASK_RUNNING:进程正在CPU上运行,或者在运行队列中等待运行。
TASK_STOPPED:进程由于某些信号(例如SIGINT、SIGSTOP等)被停止。进程在等待一个恢复信号比如 SIGCONT。
TASK_INTERRUPTIBLE:进程暂停并等待某个条件得到满足。如果进程处在该状态下并接收到一个停止信号,进程的状态会改变,操作将被中断。一个典型的例子是进程等待键盘中断。
TASK_UNINTERRUPTIBLE:在该状态下会给进程发送一个不执行任何操作的信号。典型例子是进程在等待磁盘I/O操作。
TASK_ZOMBIE:一个进程通过exit()系统调用退出后,它的父进程应该知道它已终止。该状态下,一个进程在等待通知它的父进程释放所有的数据结构。
僵尸进程:当一个进程接收到一个终止信号时,在结束之前一般需要一些时间结束所有的任务(比如关闭打开的文件)。通常在很短的时间内,这个进程是一个僵尸进程。在进程完成所有的关闭任务之后,它将相关终止报告发给父进程。有时候,一个僵尸进程不能终止自己,在这种情况下其显示为Z(僵尸)状态。使用kill命令是不能杀死这样一个进程的,因为它已经被认定为死亡。如果你无法摆脱一个僵尸进程,你可以杀死父进程,这样僵尸就会随之消失。注意:init进程是一个非常重要的进程,如果僵尸进程的父进程是init,那么需要重新启动系统来摆脱僵尸进程。
进程使用自己的内存地址区域来执行工作,工作的变化取决于当前情况和进程的使用,一个进程有不同的工作负载和不同需求的数据大小,所需要的内存大小也不一样。怎么解决这个问题?Linux内核对每个进程采用的是动态内存分配机制。
进程的内存区域由以下3段组成:
1) 文本段:存储可执行代码。
2) 数据段:数据段由3个区域组成:
2.1) 初始化数据,比如静态变量。
2.2) BSS零初始化数据:数据初始化为零。
2.3) 堆(Heap):malloc()会根据需求动态分配内存。堆向着较高的地址增长。
3) 堆栈段(Stack segment):存储局部变量、函数参数、返回的存储函数的存放区域。堆栈向着较低地址增长。
怎么查看一个用户态进程的内存地址空间分配情况?pamp命令。使用ps命令可以显示段的总共大小。
一个单独的CPU在一个时间只能执行一个程序。Linux使用多任务处理(multitasking)机制,系统中多个程序可以同时运行。在多任务处理(multitasking)机制下,多个程序共享CPU,在CPU上轮流运行。
内核使用进程调度程序来确定哪个程序在哪个给定时间点运行。为了工作正常,进程调度程序必须合理调度不同的资源。必须很快确定接下来轮到哪个进程得到CPU。通常进程调度程序必须保证各进程得到的CPU时间是公平的,但是允许高优先级进程得到更大的CPU时间,或许可以抢占较低优先级进程的CPU时间。进程调度程序必须对交互式应用程序做出响应。最后在多种多样的负载条件下进程调度程序应该表现出可预见性和可扩展性,如同给系统添加额外的程序。
1) O(1)调度程序
O(1)调度程序是在Linux 2.6内核中引进的,比如 红帽企业版Linux 4和Linux 5。以前的调度程序在O(n)时间里操作,必须扫描整个进程列表,以便找到下一个要运行的进程。这不能很好地扩展拥有大量进程的系统。O(1)调度程序工作时每个CPU有2个队列:一个运行队列和一个过期的队列。调度程序根据进程的系统优先级将进程放到运行队列的进程列表中,需要调度时,取出运行队列中最高优先级列表中的第一个进程并运行。调度程序基于进程的优先级和以前的阻塞率给进程分配一个时间片,当进程时间片用完后,进程调度程序将其移动到过期队列相应的优先级列表中,然后从运行队列中取出下一个具有最高优先级的进程,重复以上过程。一旦运行队列中不再有进程等待,调度程序从过期队列移动到新的运行队列。
一般交互式进程(相对于实时进程)有机会得到较高的优先级,拥有较长的时间片,比低优先级的进程有更多的计算时间,但并不会导致完全饿死低优先级进程。这种算法的优点是极大地提高了可扩展性。企业级工作负载通常包括大量的线程和进程,并且也有相当数量的处理器。新的O(1) CPU调度程序在2.6内核中被设计出来,但又可向前移植到2.4内核系列中。
新的调度程序另一个显著的优势是支持非统一内存架构NUMA(Non-Uniform Memory Architecture)和对称多线程处理器(SMP),比如Intel超线程(HT)技术(Intel Hyper-Threading technology)。NUMA确保了负载均衡不会在NUMA节点之间发生,除非一个节点负担过重。这种机制确保了流量相对缓慢的可伸缩性链路在NUMA系统中达到最小化。
2) 完全公平调度程序
Completely Fair Scheduler(CFS)在Linux 2.6.23内核版本第一次引入,例如在红帽企业版本Linux6中,用来替代O(1)调度程序。CFS基于“虚拟时间”的红黑树,虚拟时间是基于进程等待运行的时间、竞争CPU的进程数量以及进程的优先级来计算的。具有最多虚拟时间的进程得到CPU的使用权限。一旦进程不再拥有最多的虚拟时间,它将被拥有最多虚拟时间的进程抢占。
1) 为什么会有进程和线程?
划分资源管理的最小单元,以便管理整个系统的CPU/内存等资源,提高资源使用率。内存资源是有限的,只有当前运行的进程才加载到内存中。CPU资源也是有限的,如果当前运行的进程一直霸占着资源不释放会饿死其它进程,取出当前队列中系统优先级高的进程,根据计算出的该进程的时间片来使用CPU资源。
2) 线程如何避免共享资源的冲突?
线程们共享相同的资源,比如内存、地址空间、打开的文件等。因为共享资源,所以每个线程不能同时改变共享的资源,因此互斥、锁、序列化等是用户应用程序要实现的机制。
3) 如何进行上下文切换?
在CPU执行期间,运行进程的信息被存储在CPU的寄存器和高速缓存(cache)中,执行的进程被加载到寄存器的数据集被称为上下文(context)。在切换过程中,先存储运行进程的上下文,然后将下一个待运行进程的上下文恢复到寄存器。进程描述符和内核模式堆栈区域用于存储上下文,这个切换的过程被称为上下文切换(context switching)。
4) 如何进程不同需求场景下的内存需求?
了解进程的内存结构,仅数据段的堆根据mac()根据需求动态分配。
5) CPU如何调度程序的?
调度程序在O(n)时间,必须扫描整个进程列表,以便找到下一个要运行的进程。
O(1)调度程序工作时每个CPU有2个队列:一个运行队列和一个过期的队列。
CFS基于“虚拟时间”的红黑树,虚拟时间是基于进程等待运行的时间、竞争CPU的进程数量以及进程的优先级来计算的。
读书笔记来自赵永刚老师的《Linux性能优化大师》,如有侵权,请通知删除。