步遥情感网
您的当前位置:首页Linux 学习笔记 - 进程

Linux 学习笔记 - 进程

来源:步遥情感网
Linux 进程

1 基本内容

进程,作为Linux里面的两大基本抽象之一,笼统来说就是执行中的程序。这样一个概念涵盖的内容很丰富:汇编代码、数据、状态、各种资源(包括文件、信号、内存等等)。

本文档未包含进程间通信、同步等内容。

1.1 标识一个进程

每一个进程,在内核中,都对应了一个task_struct结构体,也称为进程描述符,其中包含了描述一个进程的所有信息(约1.7K字节)。

在内核中,所有的的task_struct都被组织在任务队列这样一个双向链表中,如下图:

操作系统通过进程ID pid 来区分不同的进程描述符,该变量为了与老版本的Linux兼容,其最大值的默认值被默认设置为32768。这样的取值对于一般的个人机是足够使用了,但是有些大型的服务器可能会需要更多的进程。

这个最大值可以进行设置,如果不考虑兼容性,可以设置成超过32768。

既然task_struct记录了所有进程相关的描述信息,不难想象内核大部分对进程的操作都是通过该结构进行,那么高效的访问task_struct就十分重要了。X86体系中,内核访问task_struct的示意图如下:

在进程的内核栈的最底端有一个thread_info结构,其保存了task_struct的地址。宏current_thread_info()会取到本进程的thread_info,则current_thread_info()->task_struct即可。

current_thread_info的实现值得注意:将栈指针SP的后12 / 13位与0相与。两种取值分别对应了4k / 8k 的内核栈。

1.2 进程状态

Linux进程有5种状态:

 TASK_RUNNING:运行状态 – 正在执行或者在等待执行的任务队列中  TASK_INTERRUPTIBLE:进程睡眠(或者说阻塞)

 TASK_UNINTERRUPTIBLE:不可中断 – 等待时不可被信号唤醒,也因此较上一种使用的较

 TASK_ZOMBIE:僵尸 – 进程已经结束,但为了父进程能够获得相关信息,内核还保留了该

进程的一些相关信息,– 直到父进程调用wait4()

 TASK_STOPPED / TASK_TRACED:暂停执行、调试状态 – 一般都是通过特定的信号来实

现本状态与运行状态的切换 下面是FSM:

2013-03-24

Page 2 , Total 11 第2页,共11页

1.3 进程上下文

当一个进程执行了系统调用,或者发生了某个异常,此时它将陷入内核空间,我们称内核将“代表进程执行”,并且处于进程上下文中。

进程上下文中,current宏是有效的。

当调用结束或者异常处理结束,程序恢复在用户空间的继续执行。 中断有专门的中断上下文。

1.4 进程的创建 1.4.1 进程体系

除了init进程之外,其他的进程都有其父进程(ppid)。每一个进程都有其所属的用户以及进程组,这是为了实现访问控制。

另外注意的是,在Linux中,创建进程和加载可执行文件是两个步骤,分别通过fork系列函数已经exec系列函数调用进行。

1.4.2 fork

fork用来创建子进程,子进程与父进程在以下几点上是不同的:

 pid / ppid

 各类资源统计信息清零 2013-03-24

Page 3 , Total 11 第3页,共11页

 所有挂起的信号被清除,子进程不进行继承  所有文件锁不会被继承

Linux通过clone()系统调用来实现fork,这个系统调用的命名很贴切,这么说的原因在下面会看到。

fork调用有2个返回值:父/子进程的pid。内核通常倾向于先返回子进程的pid。 子进程在被创建初始,和父进程平分剩余的时间片。

1.4.3 exec

当子进程创建成功并且被调度执行时,一般都会调用exec系列函数将目标可执行文件加载到内存中。通常,exec系列函数调用都不会返回,成功的调用回跳到新的程序入口点进行执行,而之前运行的代码是不会存在于新的地址空间中的。

exec调用改变了以下的进程信息:

 挂起的信号丢失, 锁定的内存丢失,与进程内存相关的信息丢失,包括文件映射  多数进程统计信息清零,多数线程属性还原到默认值  信号处理函数还原到默认的函数

 最重要的是,改变了地址空间和进程的映像

继承了的东西则有文件描述符集。

1.4.4 写时拷贝

前面提到clone这个系统调用名称很贴切,原因就在于,Linux内核通过写时拷贝技术来进行子进程的创建。

为什么要使用写时拷贝?早期的内核其实是这么干的:调用 fork 时,内核会把所有内部数据复制一份,复制进程的页表项,把父进程地址空间中的内容逐页复制到子进程的地址空间中。对于内核而言,主页拷贝是效率十分低下,十分耗时的操作。

写时拷贝怎么实现?在创建进程时,并不去拷贝,而是通过指针指向父进程的页表项,子进程和父进程共享该表项,其他资源也是如此。只有当有进程要进行写操作时,才会真的进行拷贝。

这样的实现,蕴含了惰性算法的思想:将代价最大的工作尽可能的向后拖延。这就很大程度上减轻了系统的拷贝开销,甚至有可能不需要进行拷贝。

写时拷贝一般是基于页的,现代处理器一般都提供硬件级别的页拷贝。当需要进行拷贝时,硬件将产生一个缺页中断,然后内核的处 理方式就是透明的拷贝该页,进程是不感知的。

最后还有一个问题要说明:内核为什么趋向于fork优先返回子进程的pid?原因在于,创建进程通常的应用场景是,当子进程创建成功后,即调用exec加载新的可执行文件,即子进程这个缺页中断是跑不掉的,而如果先返回了父进程,而父进程发生了写操作,就会导致一次额外的缺页

2013-03-24

Page 4 , Total 11 第4页,共11页

中断以及拷贝。然而遗憾的是,有时候即使是内核的愿望也有实现不了的时候。 ^_^

1.5 会话和进程组

会话和进程组的关系见下图:一般前台进程组的领头进程是登陆shell。

一般而言,当用户在某个终端上登录时,一个新的会话就开始了。会话将登陆与控制终端联系起来,每个会话都有自己的标准输入输出;进程组则提供了想组内所有进程发送信号的机制。

在系统中,存在着多个会话:除了用户登陆生成会话之外,守护进程也自成会话,从而避免与其他的会话发生联系。

1.6 进程的终止 1.6.1 结束进程

一般而言,终止进程时,只要调用exit即可。exit函数做的是进程的析构,主要做以下工作:

 释放资源(内核的,用户的)、关闭流、文件

 向父进程发送信号,把子进程的父进程设置为进程组中的其他进程  将状态设置为TASK_ZOMBIE  调用schedule()进行进程切换

1.6.2 孤儿进程

当一个进程结束时,必须有机制来保证其子进程能够找到新的父亲。否则,这些子进程将在结束时,成为真正的僵尸,浪费系统的资源

一般的做法是,在当前进程组中,另外找一个进程作为子进程的新父亲,如果找不到,就让init进程作为孤儿们的新爹。

2013-03-24

Page 5 , Total 11 第5页,共11页

2 Linux进程调度

进程调度是Linux这样的多任务操作系统的基础,只有合理的调度,才能够充分发回多任务操作系统的优势,最大限度的实现任务的并发执行。

多任务操作系统可分为两类:可抢占/不可抢占(协同)。后者需要任务主动释放资源,前者则能够由调度程序来决定什么时候什么任务被挂起并转而执行其他的任务。Linux属于前者。

2.1 调度策略 2.1.1 进程分类

在考虑调度策略之前,我们首先给进程分为两大类:处理器消耗型进程 和 I/O消耗型进程。 对前者而言,典型的就是数值计算型进程;后者典型的进程则有类似cp这类文件操作的进程。当然也有很多进程能够表现出混合类型的特质,比如Xwindow, 比如vi 等。

2.1.2 策略

不同类型的进程,其对系统资源的需要也大相径庭,自然需要有不同的调度策略。

对处理器消耗型进程,一般都没有什么I/O需求,就是一直在运行代码。因此这样的进程不应该被经常运行,考虑每次运行时适当增加运行时间比较好。

对于I/O消耗型进程,一般需要更多的时候处于运行状态,然后等待I/O事件发生,然后短短的运行一会儿(一般等待I/O到最后总会阻塞)。

调度策略的最理想情况就是获得最好的进程响应速度,系统的最大吞吐量,要在这两者之间找到平衡点。

当然,相比而言,I/O交互性进程会为系统所偏好,因为其部分代表了用户体验。想象一下,在一个登录终端上敲键盘然后半天没有响应,或者看视频的时候发现无法流畅的编辑文本了,那是多么让人崩溃的事情。

2.2 调度算法 2.2.1 优先级与时间片

在调度算法之前,首先要了解标题里面的两个概念:进程优先级,时间片。

优先级:调度算法中最基本的就是根据优先级来调度,优先级高的进程优先执行。Linux系统中,有两类优先级:nice值,以及实时进程优先级。另外Linux提供基于动态优先级的调度算法:一开始会指定进程的优先级,后续会根据进程的表现来动态调整优先级。

时间片:被调度执行的进程显然不能一直的执行下去,系统会给所有运行状态的进程分配时间片,不同优先级的进程分到的时间片长短是不一样的。一般优先级较高的进程分配到的时间片

2013-03-24

Page 6 , Total 11 第6页,共11页

更长。如下图:

时间片长短对调度性能的影响:如果时间片过短,势必由于频繁调度而增大系统开销;时间片过长,则会导致系统的交互性能受到影响。

2.2.2 O(1)调度算法

Linux进程调度算法十分之优秀,其时间复杂度达到了完美的O(1)!下面就来看看Linux是怎么做到这一点的。

首先,Linux默认定义了140(MAX_PRIO)个优先级,然后通过一个140位的bitmap表示之。接下来,对于每一个CPU,都有一个可执行队列,其中与此相关的是两个优先级数组,结构如下:

可以发现,优先级相同的进程,是以链表的形式串起来的。

开始时,所有位都为0,当有优先级的进程变为TASK_RUNNING时,对应的bit置位。当进行进程调度时,系统所要做的,仅仅是找到第一个置位的bit位,然后在活跃优先级数组的对应的优

2013-03-24

Page 7 , Total 11 第7页,共11页

先级队列中取第一个进程,即为被调度的下一个进程。从以上的描述就可以发现,在查找进程这一块,确实是做到了与系统的负载无关。

但仅仅这样还不够。我们知道,调度进程,不仅仅要决定执行哪个进程,还要决定执行多长时间。前面也提到了,Linux是能够根据进程的表现来调整优先级的,那么显然每次时间片都是要重新计算的,如果逐个计算,那么最坏的情况就是O(n)了!这里就体现到前面那个加黑的两作用了,相当的巧妙:

Linux在两个优先级数组中,分别存放了还有时间片未使用完的进程,以及本轮时间片已经使用完的进程,各自按照上述规则hash存储。当当前的active数组中所有进程用光了自己的时间片后,Linux直接交换两个优先级数组!华丽丽的O(1)啊!

2.3 抢占与上下文切换

一次抢占,实际上就是运行了一次调度程序,最高优先级的进程将得到执行。调度程序主要在两个地方被调用:有进程的状态变成TASK_RUNNING,或者有进程的时间片耗尽。

另外值得注意的是,抢占不仅仅限于用户进程,内核进程同样能够被抢占。也就是说,Linux支持完整的抢占,只要内核代码是安全的,那么就能够支持被抢占。

上下文切换主要要做两件事情:

把虚拟内存从上一个进程中切换到下一个进程。 恢复、保存栈信息以及寄存器信息。

2.4 实时进程

对于一个实时系统而言,延时,抖动和截止时间是很重要的几个指标。

另外,实时分为硬实时和软实时。所谓硬实时,是指如果延时超过了截止时间,则会导致严重的后果;软实时则相反,对超过截止时间的容错度更高。Linux属于软实时。

Linux提供两种实时进程的调度方式:SCHED_FIFO / SCHED_RR(先进先出 / 时间片轮转)。RR实际上就是使用了时间片的FIFO。低优先级的FIFO进程只能被高优先级的FIFO进程以及RR进程抢占。

2.4.1 确定性

对于进程而言,显然是希望能够看到确定的结果(给定相同的输入,一个动作总是在相同的时间内产生相同的结果)。但是现代操作系统的设计,存在了太多的不确定因素:多级缓存、多处理器、分页、交换、多任务···

Linux主要通过这几个手段进行相关的优化:内存锁定、设置处理器亲和度、故障预测等。

2.4.2 注意事项

2013-03-24

Page 8 , Total 11 第8页,共11页

1、 小心约束循环,一旦没有中断或者高优先级进程来打断,那在循环结束之前都会一直运行。如果是无限循环,那系统就失去响应了。

2、 3、 4、

不要过分占用其他进程的运行时间。

小心忙等待,如果等待一个更低优先级的进程占有的资源,那会永远处于忙等待。 永远开着一个终端,以便在必要的时候杀死进程。

2.5 其他

还有一些与进程调度相关的内容,我们应该知道。

2.5.1 动态优先级计算

前面提到了,Linux是能够提供动态优先级算法的,其基本思路是:在进程创建的时候指定一个静态优先级(所谓静态,即创建后不能更改 / 比如nice值),接下来会根据进程的表现(处理器消耗 or I/O 消耗)进行动态调整(-5 ~ +5)

那么系统如何来判断进程属于哪一类呢?是通过对进程的运行时间以及休眠(阻塞)时间的记录来进行的:如果一个进程总是处于休眠,那么其很有可能就是一个I/O消耗型进程;反之,若一个进程总是用光了自己的时间片,那自然是一个处理器消耗型的进程。对于前者,系统会调低进程的优先级取值(值越低优先级越高),提高其交互性能;反之亦然。

2.5.2 时间片计算

时间片的计算则相对简单,系统根据静态优先级来分配不同长度的时间片。Linux提供了一种巧妙的O(1)时间片计算的算法,前面也提到了。

值得注意的是:

创建进程时,父子进程将平分剩下的时间片,这是为了防止进程通过创建子进程来攫取时间片。

I/O交互性很强的进程,在时间片消耗完之后,不会被放到过期数组中(当活动数组中还有进程时,过期数组中的进程是得不到执行的),这样处理是为了避免I/O得不到响应。

2.5.3 处理器亲和度

处理器亲和度的实质,是描述了进程对缓存的依赖程度。这个概念是为了下面的负载均衡做准备的。

2.5.4 负载均衡

Linux是支持多处理器的,除了启动进程,其他进程都能被调度程序根据不同处理器的负载状况进行调度。这里主要要考虑两个问题:

2013-03-24

Page 9 , Total 11 第9页,共11页

调度程序必须充分利用系统的处理器,尽量避免处理器空闲;同时调度程序也要控制调度的频率,将处理器之间的进程迁移的性能损失降到合理的程度。

性能损失:这里就体现出处理器亲和度这个概念的重要性了,现在的对称多处理机(SMP)系统的设计上,每个处理器的缓存都是的,且同一个进程的缓存数据同一时刻只能在唯一一个处理器上生效。进程的迁移,将导致缓存失效,需要重新加载。

Linux的做法是:尽可能的将同一个进程持续的调度到同一个处理器上(软亲和度),只有当负载极度不平衡的时候(比如一个满负荷,另一个空闲)才会去进行进程的迁移。同时,如果进程非常依赖缓存,那么用户可以指定必须运行在哪个处理器,避免被迁移(硬亲和度)。

3 其他

还有一些与Linux进程相关的内容,我们也是需要了解的。

3.1 Linux进程的虚拟机制

Linux提供了两种虚拟机制

虚拟处理器:虽然实际上可能是许多进程在共享一个处理器,但系统会让进程认为自己在独享处理器。(通过进程调度)

虚拟内存:进程在获取、使用内存时,觉得自己拥有系统的所有内存。

3.2 Linux线程

现代操作系统中,线程是这样一种抽象:提供在同一程序内共享内存地址空间的机制。线程还可以共享打开的文件以及其他资源,线程支持并发,在多核系统上也能够支持并行处理。

Windows或者Sun Solaris等提供专门的线程机制(所谓轻量级进程LWP),线程被抽象成一种消耗资源少,运行迅速的执行单元。

Linux的线程机制则与此有很大的不同。对内核而言,并没有所谓的线程,线程只是提供了一种共享资源的手段,其他的方面与进程没有任何区别。

从数据结构的组织上也可以看到这两者的差异:

Windows在创建含有2个线程的进程时,在进程描述符中包含了2个线程的描述符,同时也描述了线程的共享资源,然后每个线程的描述符内再去描述各自独占的资源。

Linux则是仅仅在创建线程的时候指定那些资源共享即可,没有专门的所谓线程描述符。 因此,有时候看到内核进程、内核线程这样的说法,其实没有实质上的区别。都是指永远运行在内核空间的进程(线程)。

3.3 守护进程

守护进程通常在系统启动的时候开始运行,执行一些系统级的任务。平时一般处于休眠状

2013-03-24

Page 10 , Total 11 第10页,共11页

态,一旦等待的目标时间发生则会被唤醒并进行处理。

习惯上守护进程以d(daemon)结尾,但这个并不是什么标准。

守护进程的两个基本要求:必须是init的子进程,必须不与任何控制终端相关联。 创建守护进程步骤: 1、 创建新进程daemon

2、 父进程调用exit,使得daemon的父进程不再运行,daemon不是进程组组长

3、 调用setsid,创建自己的会话和进程组,并自己成为组长(保证了不会与终端相关联) 4、 调用chdir改变工作目录到根目录(第一步时继承了父进程的工作目录) 5、 关闭文件所有可确认的文件描述符,并把0,1,2重定向到 /dev/null

3.4 进程资源

Linux内核对进程能够占用的资源是有的,是强制性的,如果超过了,则资源的申请将会失败。

列表:

 进程地址空间上限

 内存转储文件大小的最大值 – 所谓 core dump 文件  一个进程可使用的最长cpu时间  进程数据段 和 堆 的大小

 可创建的最大文件,包括扩展文件大小  进程可以拥有的最大文件锁个数  内存锁定的最大字节数

 POSIX消息队列中最多可以分配的字节数  进程降低NICE值的最大值  可打开的最大文件数

 系统同一时刻所允许的最大进程数  进程可驻留在内存中的最大页数

 非root权限进程可以拥有的最大实时优先级  用户消息队列中最多信号个数  栈的最大字节数

2013-03-24

Page 11 , Total 11 第11页,共11页

因篇幅问题不能全部显示,请点此查看更多更全内容