读书笔记--深入分析Linux内核源码-第四章 进程描述

 

深入分析Linux内核源码-第四章 进程描述

Sailor_forever 整理 sailing_9806@163.com  转载请注明

http://blog.csdn.net/sailor_8318/archive/2008/05/16/2452983.aspx

 

 

【摘要】本章详解了task_struct结构,介绍了进程的各种状态,分析了进程的组织形式,针对不同的进程状态介绍了运行队列和等待队列;介绍了通用链表list_head面向对象思想在C中的具体实现,以及在此基础上运行的等待队列机制;最后简单介绍了内核编程的信号量、原子操作以及自旋锁机制。

【关键字】task_struct,进程状态,运行队列,等待队列,list_head,信号量,原子操作,自旋锁

 

第四章 进程描述... 1

4.1进程和程序(Process and Program... 1

4.2 Linux中的进程概述... 2

4.3 task_struct结构描述... 3

4.4 task_struct结构在内存中的存放... 9

4.4.1进程内核栈... 9

4.4.2 当前进程(current宏)... 10

4.5 进程组织的方式... 10

4.5.1哈希表... 10

4.5.2双向循环链表... 10

4.5.3 运行队列... 11

4.5.4等待队列... 12

4.6 内核线程... 16

4.7进程的权能... 17

4.8内核同步... 18

4.8.1信号量... 18

4.8.2原子操作... 18

4.8.3 自旋锁、读写自旋锁和大读者自旋锁... 19

4.9 本章小节... 20

 

第四章 进程描述

4.1进程和程序(Process and Program

首先我们对进程作一明确定义:所谓进程是由正文段(text)、用户数据段(user segment)以及系统数据段(system segment)共同组成的一个执行环境。


程序只是一个普通文件,是一个机器代码指令和数据的集合,这些指令和数据存储在磁盘上的一个可执行映象(Executable Image)中,所以,程序是一个静态的实体。源程序中你要定义许多变量,在可执行文件中,这些变量就组成了数据段的一部分;源程序中的许多语句,例如“ i++; for(i=0; i<10; i++);”等,在可执行文件中,它们对应着许多不同的机器代码指令,这些机器代码指令经CPU执行,就完成了你所期望的工作。进程可以认为是运行中的程序,它除了包含程序中的所有内容外,还包含一些额外的数据。程序和进程的组成如图4.1所示。

 

                    4.1 程序及进程的组成

程序装入内存后就可以运行了:在指令指针寄存器的控制下,不断的将指令取至CPU运行。程序的执行过程实际上就是一个执行环境的总和,这个执行环境包括程序中各种指令和数据,还有一些额外数据,比如说寄存器的值、用来保存临时数据(例如传递给某个函数的参数、函数的返回地址、保存变量等)的堆栈(包括程序堆栈和系统堆栈)、被打开文件的数量及输入输出设备的状态等等。这个执行环境的动态变化表征程序的运行。我们就把这个环境称作“进程”,它代表程序的执行过程,是一个动态的实体,它随着程序中指令的执行而不断地变化。

Linux是一个多任务操作系统,也就是说,可以有多个程序同时装入内存并运行,操作系统为每个程序建立一个运行环境即创建进程,每个进程拥有自己的虚拟地址空间,它们之间互不干扰,即使要相互作用,也要通过内核提供的进程间通信机制(IPC)。Linux内核支持多个进程虚拟地并发执行,这是通过不断地保存和切换程序的运行环境而实现的,选择哪个进程运行是由调度程序决定的。把“进程切换”(Process Switching)称为“环境切换”或“上下文切换”(Context Switching,这些术语表达的意思是相同的。

进程运行过程中,还需要其他的一些系统资源,由于这些系统资源是由所有进程共享的,所以Linux必须监视进程和它所拥有的系统资源,使它们们可以公平地拥有系统资源以得到运行。

系统中最宝贵的资源是CPU,操作系统通过调度程序来选择下一个最应该运行的进程,并使用一系列的调度策略来确保公平和高效。

进程是一个动态实体,如图4.1所示,进程实体由三个独立的部分组成:

1)正文段(text):存放被执行的机器指令。这个段是只读的,它允许系统中正在运行的两个或多个进程之间能够共享这一代码。

2)用户数据段(user segment):存放进程在执行时直接进行操作的所有数据,包括进程使用的全部变量在内。显然,这里包含的信息可以被改变。虽然进程之间可以共享正文段,但是每个进程需要有它自己的专用用户数据段。

3)系统数据段(system segment):该段有效地存放程序运行的环境。事实上,这正是程序和进程的区别所在。其是进程实体最重要的一部分,之所以说它有效地存放程序运行的环境,是因为这一部分存放有进程的控制信息。系统中有许多进程,操作系统要管理它们、调度它们运行,就是通过这些控制信息。Linux为每个进程建立了task_struct数据结构来容纳这些控制信息。

总之,进程是一个程序完整的执行环境。该环境是由正文段、用户数据段、系统数据段的信息交织在一起组成的。

4.2 Linux中的进程概述

Linux中的每个进程由一个task_struct数据结构来描述,在Linux中,任务(task)、和进程(process)是两个相同的术语,task_struct其实就是通常所说的“进程控制块”即PCBtask_struct容纳了一个进程的所有信息,是系统对进程进行控制的唯一手段,也是最有效的手段。

Linux2.4中,Linux为每个新创建的进程动态地分配一个task_struct结构。Linux支持多处理机(SMP),所以系统中允许有多个CPU Linux作为多处理机操作系统时系统中允许的最大CPU个数为32。很显然,Linux作为单机操作系统时,系统中只有一个CPU,本书主要讨论单处理机的情况。

和其他操作系统类似,Linux也支持两种进程:普通进程和实时进程。实时进程具有一定程度上的紧迫性,要求对外部事件做出非常快的响应;而普通进程则没有这种限制。所以,调度程序要区分对待这两种进程,通常,实时进程要比普通进程优先运行。这两种进程的区分也反映在task_struct数据结构中了。

总之,包含进程所有信息的task_struct数据结构是比较庞大的,但是该数据结构本身并不复杂,我们将它的所有域按其功能可做如下划分:

·进程状态(State

·进程调度信息(Scheduling Information

·各种标识符(Identifiers

·进程通信有关信息(IPCInter_Process Communication

·时间和定时器信息(Times and Timers

·进程链接信息(Links

·文件系统信息(File System

·虚拟内存信息(Virtual Memory

·页面管理信息(page

·对称多处理器(SMP)信息

·和处理器相关的环境(上下文)信息(Processor Specific Context

·其它信息

下面我们对task_struct结构进行具体描述。

4.3 task_struct结构描述

 1. 进程状态(State

进程执行时,它会根据具体情况改变状态。进程状态是调度和对换的依据Linux中的进程主要有如下状态,如表4.1所示。

4.1                 Linux进程的状态

 

内核表示

含义

TASK_RUNNING

可运行

TASK_INTERRUPTIBLE

可中断的等待状态

TASK_UNINTERRUPTIBLE

不可中断的等待状态

TASK_ZOMBIE

僵死

TASK_STOPPED

暂停

 

·运行状态

处于这种状态的进程,要么正在运行、要么正准备运行(只是可运行,并不表示其正在运行,注意和其他实时系统的区别,其包括了通常所说的运行态和就绪态)。正在运行的进程就是当前进程(由current所指向的进程),而准备运行的进程只要得到CPU就可以立即投入运行,CPU是这些进程唯一等待的系统资源。系统中有一个运行队列(run_queue),用来容纳所有处于可运行状态的进程,调度程序执行时,从中选择一个进程投入运行。在后面我们讨论进程调度的时候,可以看到运行队列的作用。当前运行进程一直处于该队列中,也就是说,current总是指向运行队列中的某个元素,只是具体指向谁由调度程序决定。

·等待状态

处于该状态的进程正在等待某个事件(event)或某个资源,它肯定位于系统中的某个等待队列(wait_queue)中Linux中处于等待状态的进程分为两种:可中断的等待状态和不可中断的等待状态。处于可中断等待态的进程可以被信号唤醒,如果收到信号,该进程就从等待状态进入可运行状态,并且加入到运行队列中,等待被调度;而处于不可中断等待态的进程是因为硬件环境不能满足而等待,例如等待特定的系统资源,它任何情况下都不能被打断,只能用特定的方式来唤醒它,例如唤醒函数wake_up()等。

·暂停状态

此时的进程暂时停止运行来接受某种特殊处理。通常当进程接收到SIGSTOPSIGTSTPSIGTTIN SIGTTOU信号后就处于这种状态。例如,正接受调试的进程就处于这种状态

·僵死状态

进程虽然已经终止,但由于某种原因,父进程还没有执行wait()系统调用,终止进程的信息也还没有回收。顾名思义,处于该状态的进程就是死进程,这种进程实际上是系统中的垃圾,必须进行相应处理以释放其占用的资源。

 

2. 进程调度信息

调度程序利用这部分信息决定系统中哪个进程最应该运行,并结合进程的状态信息保证系统运转的公平和高效。这一部分信息通常包括进程的类别(普通进程还是实时进程)、进程的优先级等等。如表4.2所示:

4.2                  进程调度信息

域名

含义

need_resched

调度标志

Nice

静态优先级

Counter

动态优先级

Policy

调度策略

rt_priority

实时优先级

 

need_resched被设置时,在“下一次的调度机会”就调用调度程序schedule() counter代表进程剩余的时间片,是进程调度的主要依据,也可以说是进程的动态优先级,因为这个值在不断地减少;nice是进程的静态优先级,同时也代表进程的时间片,用于对counter赋值,可以用nice()系统调用改变这个值;policy是适用于该进程的调度策略,实时进程和普通进程的调度策略是不同的;rt_priority只对实时进程有意义,它是实时进程调度的依据。

进程的调度策略有三种,如表4.3所示。

4.3                 进程调度的策略

名称

解释

适用范围

SCHED_NORMAL

其他调度

普通进程

SCHED_FIFO

先来先服务调度

实时进程

SCHED_RR

时间片轮转调度

 

只有root用户能通过sched_setscheduler()系统调用来改变调度策略。

 

3 .标识符(Identifiers

每个进程有进程标识符、用户标识符、组标识符,如表4.4所示。

不管对内核还是普通用户来说,怎么用一种简单的方式识别不同的进程呢?这就引入了进程标识符(PIDprocess identifier),每个进程都有一个唯一的标识符,内核通过这个标识符来识别不同的进程,同时,进程标识符PID也是内核提供给用户程序的接口,用户程序通过PID对进程发号施令。PID32位的无符号整数,它被顺序编号:新创建进程的PID通常是前一个进程的PID1

4.4                          各种标识符   

域名

含义

Pid

进程标识符

Uidgid

用户标识符、组标识符

Euidegid

有效用户标识符、有效组标识符

Suidsgid

备份用户标识符、备份组标识符

Fsuidfsgid

文件系统用户标识符、文件系统组标识符

 

另外,每个进程都属于某个用户组。task_struct结构中定义有用户标识符和组标识符。它们同样是简单的数字,这两种标识符用于系统的安全控制。系统通过这两种标识符控制进程对系统中文件和设备的访问。

 

4. 进程通信有关信息(IPCInter_Process Communication

为了使进程能在同一项任务上协调工作,进程之间必须能进行通信即交流数据。

Linux支持多种不同形式的通信机制。它支持典型的Unix 通信机制(IPC Mechanisms):信号(Signals)、管道(Pipes),也支持System V 通信机制:共享内存(Shared Memory)、信号量和消息队列(Message Queues,如表4.5

4.5                        进程通信有关信息

域名

含义

Spinlock_t sigmask_lock

信号掩码的自旋锁

Long blocked

信号掩码

Struct signal  *sig

信号处理函数

Struct sem_undo *semundo

为避免死锁而在信号量上设置的取消操作

Struct sem_queue *semsleeping

与信号量操作相关的等待队列

这些域的具体含义将在进程通信一章进行讨论。

 

5. 进程链接信息(Links

程序创建的进程具有父/子关系。因为一个进程能创建几个子进程,而子进程之间有兄弟关系,在task_struct结构中有几个域来表示这种关系。

Linux系统中,除了初始化进程init,其他进程都有一个父进程(parent process)或称为双亲进程。可以通过fork()或clone()系统调用来创建子进程,除了进程标识符(PID)等必要的信息外,子进程的task_struct结构中的绝大部分的信息都是从父进程中拷贝,或说“克隆”过来的。系统有必要记录这种“亲属”关系,使进程之间的协作更加方便。

每个进程的task_struct结构有许多指针,通过这些指针,系统中所有进程的task_struct结构就构成了一棵进程树,这棵进程树的根就是初始化进程inittask_struct结构(init进程是Linux内核建立起来后人为创建的一个进程,是所有进程的祖先进程)。表4.6是进程所有的链接信息。

4.6                   进程链接信息

名称

英文解释

中文解释 [指向哪个进程]

p_opptr

Original parent

祖先

p_pptr

Parent

父进程

p_cptr

Child

子进程

p_ysptr

Younger sibling

弟进程

p_osptr

Older sibling

兄进程

Pidhash_next

Pidhash_pprev

 

进程在哈希表中的链接

Next_task prev_task

 

进程在双向循环链表中的链接

Run_list

 

运行队列的链表

 

6. 时间和定时器信息(Times and Timers

一个进程从创建到终止叫做该进程的生存期(lifetime)。进程耗费CPU的时间由两部分组成:一是在用户模式(或称为用户态)下耗费的时间、一是在系统模式(或称为系统态)下耗费的时间。每个时钟滴答,也就是每个时钟中断,内核都要更新当前进程耗费CPU的时间信息。

 

上面所说的counter是指进程剩余的CPU时间片,也和时间有关,所以这里我们再次提及它。表4.8是进程的所有定时器。

  4.7              与时间有关的域

域名

含义

Start_time

进程创建时间

Per_cpu_utime

进程在某个CPU上运行时在用户态下耗费的时间

Per_cpu_stime

进程在某个CPU上运行时在系统态下耗费的时间

Counter

进程剩余的时间片

 

 

  4.8                      进程的所有定时器

定时器类型

解释

什么时候更新

用来表示此种定时器的域

ITIMER_REAL

实时定时器

实时更新,即不论该进程是否运行

it_real_value

it_real_incr

real_timer

ITIMER_VIRTUAL

虚拟定时器

只在进程运行于用户态时更新

it_virt_value

it_virt_incr

ITIMER_PROF

概况定时器

进程运行于用户态和系统态时更新

it_prof_value

it_prof_incr

 

进程有三种类型的定时器:实时定时器、虚拟定时器和概况定时器。这三种定时器的特征共有三个:到期时间、定时间隔、要触发的事件。到期时间就是定时器到什么时候完成定时操作,从而触发相应的事件;定时间隔就是两次定时操作的时间间隔,它决定了定时操作是否继续进行,如果定时间隔大于0,则在定时器到期时,该定时器的到期时间被重新赋值,使定时操作继续进行下去,直到进程结束或停止使用定时器,只不过对不同的定时器,到期时间的重新赋值操作是不同的。在表4.8中,每个定时器都有两个域来表示到期时间和定时间隔:valueincr,二者的单位都是时钟滴答,和jiffies的单位是一致的。虚拟定时器和概况定时器到期时由内核发送相应的信号,而实时定时器比较特殊,它由内核机制提供支持,我们将在后面讨论这个问题。

 

每个时钟中断,当前进程所有和时间有关的信息都要更新:时间片计数器counter要更新,如果counter<=0,则要执行调度程序;进程申请的延时要更新,如果延时时间到了,则唤醒该进程;所有的定时器都要更新,Linux内核检测这些定时器是否到期,如果到期,则执行相应的操作。在这里,“更新”的具体操作是不同的:对counter,内核要对它减值,而对于所有的定时器,就是检测它的值,内核把系统当前时间和其到期时间作一比较,如果到期时间小于系统时间,则表示该定时器到期。

 

Linux内核对这三种定时器的处理是不同的,虚拟定时器和概况定时器到期时,内核向当前进程发送相应的信号:SIGVTALRM  SIGPROF ;而实时定时器要执行的操作由real_timer决定,real_timetimer_list类型的变量(定义:struct timer_list real_timer),其中容纳了实时定时器的到期时间、定时间隔等信息。

 

7. 文件系统信息(File System

进程可以打开或关闭文件,文件属于系统资源,Linux内核要对进程使用文件的情况进行记录。task_struct结构中有两个数据结构用于描述进程与文件相关的信息。其中,fs_struct中描述了两个VFS索引节点(VFS inode),这两个索引节点叫rootpwd,分别指向进程的可执行映象所对应的根目录(home directory)和当前目录或工作目录。file_struct结构用来记录了进程打开的文件的描述符(descriptor)。如表4.9所示。

       4.9                             与文件系统相关的域

定义形式

解释

Sruct fs_struct *fs

进程的可执行映象所在的文件系统

Struct files_struct *files

进程打开的文件

 

在文件系统中,每个VFS索引节点唯一描述一个文件或目录,同时该节点也是向更低层的文件系统提供的统一的接口。

 

8. 虚拟内存信息(Virtual Memory

除了内核线程(kernel thread),每个进程都拥有自己的地址空间(也叫虚拟空间),用mm_struct来描述。另外Linux2.4还引入了另外一个域active_mm,这是为内核线程而引入。因为内核线程没有自己的地址空间,为了让内核线程与普通进程具有统一的上下文切换方式,当内核线程进行上下文切换时,让切换进来的线程的active_mm 指向刚被调度出去的进程的active_mm(如果普通进程的mm域不为空,则其active_mm域与mm域相同)。内存信息如表4.10所示。

4.10                   虚拟内存描述信息

定义形式

解释

Struct mm_struct *mm

描述进程的地址空间

Struct mm_struct *active_mm

内核线程所借用的地址空间

 

9.页面管理信息

   当物理内存不足时,Linux内存管理子系统需要把内存中的部分页面交换到外存,其交换是以页为单位的。有关页面的描述信息如表4.11

  4.11                     页面管理信息

  定义形式

解释

Int swappable

进程占用的内存页面是否可换出

Unsigned long min_flat,maj_flt,nswap

进程累计的次(minor)缺页次数、主(major)次数及累计换出、换入页面数

Unsigned long cmin_flat,cmaj_flt,cnswap

本进程作为祖先进程,其所有层次子进程的累计的次(minor)缺页次数、主(major)次数及累计换出、换入页面数

 

10.对称多处理机(SMP)信息

Linux2.4SMP进行了全面的支持,表4.12是与多处理机相关的几个域。

4.12 与多处理机相关的域

   定义形式

解释

 Int has_cpu

 进程是否当前拥有CPU

 Int processor

 进程当前正在使用的CPU

 Int lock_depth

 上下文切换时内核锁的深度

 

11 和处理器相关的环境(上下文)信息(Processor Specific Context

进程作为一个执行环境的综合,当系统调度某个进程执行,即为该进程建立完整的环境时,处理器(processor)的寄存器、堆栈等是必不可少的。因为不同的处理器对内部寄存器和堆栈的定义不尽相同,所以叫做“和处理器相关的环境”。当进程暂时停止运行时,处理机状态必须保存在进程的task_struct结构中,当进程被调度重新运行时再从中恢复这些环境,也就是恢复这些寄存器和堆栈的值。处理机信息如表4.13所示。

4.13                  与处理机相关的信息

定义形式

解释

Struct thread_struct *tss

任务切换状态

 

12.其它

1  struct wait_queue *wait_chldexit

  进程结束时,或发出系统调用wait4时,为了等待子进程的结束,而将自己(父进程)睡眠在该等待队列上,设置状态标志为TASK_INTERRUPTIBLE,并且把控制权转给调度程序。

2Int exit_code exit_signal;

程序的返回代码以及程序异常终止产生的信号,这些数据由父进程(子进程完成后) 轮流查询。

 

task_struct结构是进程实体的核心,Linux内核通过该结构来控制进程:首先通过其中的调度信息决定该进程是否运行;当该进程运行时,根据其中保存的处理机状态信息来恢复进程运行现场,然后根据虚拟内存信息,找到程序的正文和数据;通过其中的通信信息和其他进程实现同步、通信等合作。几乎所有的操作都要依赖该结构,所以,task_struct结构是一个进程存在的唯一标志。

 

4.4 task_struct结构在内存中的存放

4.4.1 进程内核栈

每个进程都有自己的内核栈。当进程从用户态进入内核态时,CPU就自动地设置该进程的内核栈,也就是说,CPU从任务状态段TSS中装入内核栈指针esp

 

X86内核栈的分布如图4.2所示:

0x018fbfff

 

 

    堆栈

 

 


 

0x018fb000

 

esp

 

task_struct

 

0x018fa000

 

p

 

 

 

 

 

 

 

 


         

            4.2 内核栈的分布图

 

   Intel系统中,栈起始于末端,并朝这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的,因此,esp寄存器直接指向这个内存区的顶端

在/include/linux/sched.h中定义了如下一个联合结构:

union task_union {

       struct task_struct task;

       unsigned long stack[2408];

};

从这个结构可以看出,内核栈占8kb的内存区。实际上,进程的task_struct结构所占的内存是由内核动态分配的,更确切地说,内核根本不给task_struct分配内存,而仅仅给内核栈分配8K的内存,并把其中的一部分给task_struct使用。

 

       task_struct结构与内核栈放在一起具有以下好处:

内核可以方便而快速地找到这个结构,用伪代码描述如下:

task_struct = (struct task_struct *) STACK_POINTER & 0xffffe000

避免在创建进程时动态分配额外的内存,task_struct结构的起始地址总是开始于页大小(PAGE_SIZE)的边界。

 

4.4.2 当前进程(current宏)

      当一个进程在某个CPU上正在执行时,内核如何获得指向它的task_struct的指针?上面所提到的存储方式为达到这一目的提供了方便。在linux/include/ i386current.h 中定义了current宏,这是一段与体系结构相关的代码:

static inline struct task_struct * get_current(void)

{

struct task_struct *current;

__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));

return current;

}

 

仅仅只需检查栈指针的值,而根本无需存取内存,内核就可以导出task_struct结构的地址。

current可以看作全局变量来用,例如,current->pid返回在CPU上正在执行的进程的标识符。

 

       另外,在include/ i386/processor.h中定义了两个函数free_task_struct(  ) alloc_task_struct(  ),前一个函数释放8KBtask_union内存区,而后一个函数分配8KBtask_union内存区。

 

4.5 进程组织的方式

4.5.1 哈希表

哈希表是进行快速查找的一种有效的组织方式。Linu在进程中引入的哈希表叫做pidhash 在哈希表pidhash中插入和删除一个进程时可以调用hash_ pid(  ) unhash_ pid(  )函数。对于一个给定的pid,可以通过find_task_by_pid()函数快速地找到对应的进程。

 

4.5.2 双向循环链表

哈希表的主要作用是根据进程的pid可以快速地找到对应的进程,但它没有反映进程创建的顺序,也无法反映进程之间的亲属关系,因此引入双向循环链表。每个进程task_struct 结构中的prev_tasknext_task域用来实现这种链表,如图4.4所示

4.4  双向循环链表

 

SET_LINK用来在该链表中插入一个元素:

#define SET_LINKS(p) do { /

      (p)->next_task = &init_task; /

      (p)->prev_task = init_task.prev_task; /

      init_task.prev_task->next_task = (p); /

      init_task.prev_task = (p); /

      (p)->p_ysptr = NULL; /

       if (((p)->p_osptr = (p)->p_pptr->p_cptr) != NULL) /

               (p)->p_osptr->p_ysptr = p; /

        (p)->p_pptr->p_cptr = p; /

       } while (0)

 

从这段代码可以看出,链表的头和尾都为init_task,它对应的是进程0pid0,也就是所谓的空进程,它是所有进程的祖先。这个宏把进程之间的亲属关系也链接起来。另外,还有一个宏for_each_task()

 

#define for_each_task(p) /

        for (p = &init_task ; (p = p->next_task) != &init_task ; )

这个宏是循环控制语句,注意init_task的作用,因为空进程是一个永远不存在的进程,因此用它做链表的头和尾是安全的。

 

因为进程的双向循环链表是一个临界资源,因此在使用这个宏时一定要加锁,使用完后开锁。

 

4.5.3 运行队列

当内核要寻找一个新的进程在CPU上运行时,必须只考虑处于可运行状态的进程,因为扫描整个进程链表是相当低效的,所以引入了可运行状态进程的双向循环链表,也叫运行队列(runqueue)。

运行队列容纳了系统中所有可以运行的进程,它是一个双向循环队列,其结构如下:


4. 5 进程的运行队列链表

该队列通过task_struct结构中的两个指针run_list链表来维持。队列的标志有两个:一个是“空进程”idle_task、一个是队列的长度。

有两个特殊的进程永远在运行队列中待着:当前进程和空进程。请注意,current指针在调度过程中(调度程序执行时)是没有意义的,为什么这么说呢?调度前,当前进程正在运行,当出现某种调度时机引发了进程调度,先前运行着的进程处于什么状态是不可知的,多数情况下处于等待状态,所以这时候current是没有意义的,直到调度程序选定某个进程投入运行后,current才真正指向了当前运行进程;空进程是个比较特殊的进程,只有系统中没有进程可运行时它才会被执行,Linux将它看作运行队列的头,当调度程序遍历运行队列,是从idle_task开始、至idle_task结束的,在调度程序运行过程中,允许队列中加入新出现的可运行进程,新出现的可运行进程插入到队尾,这样的好处是不会影响到调度程序所要遍历的队列成员,可见,idle_task是运行队列很重要的标志。

另一个重要标志是队列长度,也就是系统中处于可运行状态(TASK_RUNNING)的进程数目,用全局整型变量nr_running表示,在/kernel/fork.c中定义如下

int nr_running=1

nr_running0,就表示队列中只有空进程。在这里要说明一下:若nr_running0,则系统中的当前进程和空进程就是同一个进程。但是Linux会充分利用CPU而尽量避免出现这种情况。

 

4 .5.4 等待队列

2.4版本中,引入了一种特殊的链表-通用双向链表,它是内核中实现其它链表的基础,也是面向对象的思想在C语言中的应用。在等待队列的实现中多次涉及与此链表相关的内容。

1.通用双向链表

   include/linux/list.h中定义了这种链表:

struct list_head {

        struct list_head *next, *prev;

};

这是双向链表的一个基本框架,在其它使用链表的地方就可以使用它来定义任意一个双向链表,例如:

struct foo_list {

       int data;

       struct list_head list;

};

 

对于list_head类型的链表,Linux定义了五个宏:

#define LIST_HEAD_INIT(name) { &(name), &(name) }

 

#define LIST_HEAD(name) /

        struct list_head name = LIST_HEAD_INIT(name)

 

#define INIT_LIST_HEAD(ptr) do { /

        (ptr)->next = (ptr); (ptr)->prev = (ptr); /

} while (0)

 

//通过type数据结构中member的偏移量和当前list的地址回推包含该listtype类型变量首地址。

#define list_entry(ptr, type, member) /

        ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

 

//双向循环队列,已知头结点,再次遇到头节点时即遍历完毕

#define list_for_each(pos, head) /

        for (pos = (head)->next; pos != (head); pos = pos->next)

 

前三个宏都是初始化一个空的链表,但用法不同,LIST_HEAD_INIT()在声明时使用,用来初始化结构元素,第二个宏用在静态变量初始化的声明中,而第三个宏用来初始化堆中动态分配的或者已经定义过的list变量。

其中,最难理解的宏为list_entry(),在内核代码的很多处都用到这个宏,例如,在调度程序中,从运行队列中选择一个最值得运行的进程,部分代码如下:

 

static LIST_HEAD(runqueue_head);

struct list_head *tmp;

struct task_struct *p;

 

list_for_each(tmp, &runqueue_head) {

    p = list_entry(tmp, struct task_struct, run_list);

    if (can_schedule(p)) {

        int weight = goodness(p, this_cpu, prev->active_mm);

        if (weight > c)

            c = weight, next = p;

    }

}

 

  从这段代码可以分析出list_entry(ptr, type, member)宏及参数的含义:    ptr是指向list_head类型链表的指针,type为一个结构,而member为结构type中的一个域,类型为list_head,这个宏返回指向type结构的指针。

 

另外,对list_head类型的链表进行删除和插入(头或尾)的宏为list_del()/list_add()/list_add_tail(),在内核的其它函数中可以调用这些宏。例如,从运行队列中删除、增加及移动一个任务的代码如下:

static inline void del_from_runqueue(struct task_struct * p)

{

        nr_running--;

        list_del(&p->run_list);

        p->run_list.next = NULL;

}

static inline void add_to_runqueue(struct task_struct * p)

{

        list_add(&p->run_list, &runqueue_head);

        nr_running++;

}

 

static inline void move_last_runqueue(struct task_struct * p)

{

        list_del(&p->run_list);

        list_add_tail(&p->run_list, &runqueue_head);

}

 

static inline void move_first_runqueue(struct task_struct * p)

{

        list_del(&p->run_list);

        list_add(&p->run_list, &runqueue_head);

}

 

 

2.等待队列

运行队列链表把处于TASK_RUNNING状态的所有进程组织在一起。当要把其他状态的进程分组时,不同的状态要求不同的处理,Linux选择了下列方式之一:

TASK_STOPPED  TASK_ZOMBIE状态的进程不链接在专门的链表中,也没必要把它们分组,因为父进程可以通过进程的PID,或进程间的亲属关系检索到子进程。

TASK_INTERRUPTIBLE TASK_UNINTERRUPTIBLE状态的进程再分成很多类,其每一类对应一个特定的事件。在这种情况下,进程状态提供的信息满足不了快速检索进程,因此,有必要引入另外的进程链表。这些链表叫等待队列。

 

等待队列在内核中有很多用途,尤其对中断处理、进程同步及定时用处更大。进程必须经常等待某些事件的发生,例如,等待一个磁盘操作的终止,等待释放系统资源,或等待时间走过固定的间隔。等待队列实现在事件上的条件等待,也就是说,希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队列表示一组睡眠的进程,当某一条件变为真时,由内核唤醒它们。等待队列由循环链表实现。在2.4的版本中,关于等待队列的定义如下:

struct __wait_queue {

        unsigned int flags;

        struct task_struct * task;

        struct list_head task_list;

};

typedef struct __wait_queue wait_queue_t;_t表示此类型为typedef的类型)

 

另外,关于等待队列另一个重要的数据结构—等待队列首部的描述如下:

struct __wait_queue_head {

         wq_lock_t lock;

        struct list_head task_list;

};

typedef struct __wait_queue_head wait_queue_head_t;

 

 

下面给出2.4版中的一些主要函数及其功能:

init_waitqueue_head()—对等待队列首部进行初始化

init_waitqueue_entry()-对要加入等待队列的元素进行初始化

waitqueue_active()—判断等待队列中已经没有等待的进程

add_wait_queue()—给等待队列中增加一个元素

remove_wait_queue()—从等待队列中删除一个元素

注意,在以上函数的实现中,都调用了对list_head 类型链表的操作函数(list_del()/list_add()/list_add_tail()),因此说,list_head 类型相当于C++中的基类型

 

#define    SLEEP_ON_VAR                               /

       unsigned long flags;                            /

       wait_queue_t wait;                      /

       init_waitqueue_entry(&wait, current);

 

#define SLEEP_ON_HEAD                               /

       spin_lock_irqsave(&q->lock,flags);             /

       __add_wait_queue(q, &wait);                     /

       spin_unlock(&q->lock);

 

#define    SLEEP_ON_TAIL                              /

       spin_lock_irq(&q->lock);                    /

       __remove_wait_queue(q, &wait);               /

       spin_unlock_irqrestore(&q->lock, flags);

 

 

希望等待一个特定事件的进程能调用下列函数中的任一个:

sleep_on(  )函数对当前的进程起作用,我们把当前进程叫做P

sleep_on(wait_queue_head_t *q)

{

         SLEEP_ON_VAR   /*宏定义,用来初始化要插入到等待队列中的元素*/

         current->state = TASK_UNINTERRUPTIBLE;

         SLEEP_ON_HEAD  /*宏定义,把P插入到等待队列*/

         schedule();

        SLEEP_ON_TAIL   /*宏定义,把P从等待队列中删除 */

}

 

       这个函数当前进程的状态设置为TASK_UNINTERRUPTIBLE,并把P插入等待队列。然后,它调用调度程序恢复另一个程序的执行。当P被唤醒时,调度程序恢复sleep_on(  )函数的执行,把P从等待队列中删除。

interruptible_sleep_on(  )sleep_on(  )函数是一样的,但稍有不同,前者把进程P的状态设置为TASK_INTERRUPTIBLE 而不是TASK_UNINTERRUPTIBLE ,因此,通过接受一个信号可以唤醒P

sleep_on_timeout(  ) interruptible_sleep_on_timeout(  )与前面情况类似,但它们允许调用者定义一个时间间隔,过了这个间隔以后,内核唤醒进程。为了做到这点,它们调用schedule_timeout(  )函数而不是schedule(  )函数。

 

利用wake_up 或者 wake_up_interruptible宏,让插入等待队列中的进程进入TASK_RUNNING状态,这两个宏最终都调用了try_to_wake_up(  )函数:

 

    static inline int try_to_wake_up(struct task_struct * p, int synchronous)

   {

        unsigned long flags;

          int success = 0;

         spin_lock_irqsave(&runqueue_lock, flags); /*加锁*/

          p->state = TASK_RUNNING;

       if (task_on_runqueue(p))      /*判断p是否已经在运行队列*/

                 goto out;

          add_to_runqueue(p);        /*不在,则把p插入到运行队列*/

         if (!synchronous || !(p->cpus_allowed & (1 << smp_processor_id()))) 

                reschedule_idle(p);

         success = 1;

     out:

       spin_unlock_irqrestore(&runqueue_lock, flags);  /*开锁*/

       return success;

   }

在这个函数中,p为要唤醒的进程。如果p不在运行队列中,则把它放入运行队列。如果重新调度正在进行的过程中,则调用reschedule_idle()函数,这个函数决定进程p是否应该抢占某一CPU上的当前进程。在内核的其它部分,最常用的还是wake_up 或者 wake_up_interruptible宏,也就是说,如果你要在内核级进行编程,只需调用其中的一个宏。

 

4.6 内核线程

       内核线程(thread)或叫守护进程(daemon),在操作系统中占据相当大的比例,Linux操作系统启动以后,尤其是Xwindow也启动以后,你可以用”ps”命令查看系统中的进程,这时会发现很多以d”结尾的进程名,这些进程就是内核线程

   内核线程也可以叫内核任务,它们周期性地执行,在Linux中,内核线程与普通进程有一些本质的区别,从以下几个方面可以看出二者之间的差异:

内核线程执行的是内核中的函数,而普通进程只有通过系统调用才能执行内核中的函数。

内核线程只运行在内核态,而普通进程既可以运行在用户态,也可以运行在内核态。

因为内核线程指只运行在内核态,因此,它只能使用大于PAGE_OFFSET 3G )的地址空间。另一方面,不管在用户态还是内核态,普通进程可以使用4GB的地址空间。

 

内核线程是由kernel_thread(  )函数在内核态下创建的,这个函数所包含的代码大部分是内联式汇编语言,但在某种程度上等价于下面的代码:

int kernel_thread(int (*fn)(void *), void * arg,

                  unsigned long flags)

{

    pid_t p;

    p = clone( 0, flags | CLONE_VM );

    if ( p )        /* parent */

        return p;

    else {          /* child */

        fn(arg);

        exit(  );

   }

}

 

 

4.7进程的权能

    Linux用“权能(capability)”表示一进程所具有的权力。一种权能仅仅是一个标志,它表明是否允许进程执行一个特定的操作或一组特定的操作。如表4.13给出了在Linux内核中已定义的权能。

4.13 进程的权能

名字

描述

CAP_CHOWN

忽略对文件和组的拥有者进行改变的限制

CAP_DAC_OVERRIDE

忽略文件的访问许可权

CAP_DAC_READ_SEARCH

忽略文件/目录读和搜索的许可权

CAP_FOWNER

忽略对文件拥有者的限制

CAP_FSETID

忽略对setidsetgid标志的限制

CAP_KILL

忽略对信号挂起的限制

CAP_SETGID

允许 setgid标志的操作

CAP_SETUID

允许 setuid 标志的操作

CAP_SETPCAP

转移/删除对其它进程所许可的权能

CAP_LINUX_IMMUTABLE

允许对仅追加和不可变文件的修改

CAP_NET_BIND_SERVICE

允许捆绑到低于1024TCP/UDP的套节字

CAP_NET_BROADCAST

 允许网络广播和监听多点传送

CAP_NET_ADMIN

允许一般的网络管理。

CAP_NET_RAW

允许使用RAWPACKET套节字

CAP_IPC_LOCK

允许页和共享内存的加锁

CAP_IPC_OWNER

跳过IPC拥有者的检查

CAP_SYS_MODULE

允许内核模块的插入和删除

CAP_SYS_RAWIO

允许通过ioperm(  ) iopl(  )访问I/O端口

CAP_SYS_CHROOT

允许使用chroot(  )

CAP_SYS_PTRACE

允许在任何进程上使用 ptrace(  )

 CAP_SYS_PACCT

允许配置进程的计账

CAP_SYS_ADMIN

允许一般的系统管理

CAP_SYS_BOOT

允许使用reboot(  )

CAP_SYS_NICE

忽略对 nice(  )的限制

CAP_SYS_RESOURCE

忽略对几个资源使用的限制

CAP_SYS_TIME

允许系统时钟和实时时钟的操作

CAP_SYS_TTY_CONFIG

允许配置tty设备

 

   任何时候,每个进程只需要有限种权能,这是其主要优势。因此,即使一位有恶意的用户使用有潜在错误程序,他也只能非法地执行有限个操作类型。

 

 

4.8内核同步

4.8.1 信号量

  进程间对共享资源的互斥访问是通过“信号量”机制来实现的。信号量机制是操作系统教材中比较重要的内容之一。Linux内核中提供了两个函数down()up(),分别对应于操作系统教材中的PV操作。

信号量在内核中定义为semaphore数据结构,其位于include/i386/semaphore.h

struct semaphore {

         atomic_t count;

             int sleepers;

         wait_queue_head_t wait;

};

其中的count域就是“信号量”中的那个“量”,它代表着可用资源的数量。如果该值大于0,那么资源就是空闲的,也就是说,该资源可以使用。相反,如果count小于0,那么这个信号量就是繁忙的,也就是说,这个受保护的资源现在不能使用。在后一种情况下,count的绝对值表示了正在等待这个资源的进程数。该值为0表示有一个进程正在使用这个资源,但没有其他进程在等待这个资源。

   Wait域存放等待链表的地址,该链表中包含正在等待这个资源的所有睡眠的进程。当然,如果count大于或等于0,则等待队列为空。为了明确表示等待队列中正在等待的进程数,引入了计数器sleepers

down()up()函数主要应用在文件系统和驱动程序中,把要保护的临界区放在这两个函数中间,用法如下:

down();

临界区

up();

  这两个函数是用嵌入式汇编实现的,非常麻烦,在此不予详细介绍。

 

4.8.2 原子操作

   避免干扰的最简单方法就是保证操作的原子性,即操作必须在一条单独的指令内执行。有两种类型的原子操作,即位图操作和数学的加减操作。

 1.位图操作

在内核的很多地方用到位图,例如内存管理中对空闲页的管理,位图还有一个广泛的用途就是简单的加锁,例如提供对打开设备的互斥访问。关于位图的操作函数如下:

以下函数的参数中,addr指向位图。

void set_bit(int nr, volatile void *addr):设置位图的第nr位。

void clear_bit(int nr, volatile void *addr): 清位图的第nr

void change_bit(int nr, volatile void *addr): 改变位图的第nr

int test_and_set_bit(int nr, volatile void *addr): 设置第nr位,并返回该位原来的值,且两个操作是原子操作,不可分割。

int test_and_clear_bit(int nr, volatile void *addr): 清第nr为,并返回该位原来的值,且两个操作是原子操作。

int test_and_change_bit(int nr, volatile void *addr):改变第nr位,并返回该位原来的值,且这两个操作是原子操作。

这些操作利用了LOCK_PREFIX宏,对于SMP内核,该宏是总线锁指令的前缀,对于单CPU这个宏不起任何作用。这就保证了在SMP环境下访问的原子性。

 

 2.算术操作

有时候位操作是不方便的,取而代之的是需要执行算术操作,即加、减操作及加1、减1作。典型的例子是很多数据结构中的引用计数域count(如inode结构)。这些操作的原子性是由atomic_t数据类型和表4.14中的函数保证的。atomic_t的类型在include/ i386/atomic.h定义如下:

   typedef struct { volatile int counter; } atomic_t;

 

4.14 原子操作

函数

说明

atomic_read(v)

返回*v

atomic_set(v,i)

*v设置成i

Atomic_add(I,v)

*v增加i

Atomic_sub(I,v)

*v中减去I

Atomic_inc(v)

*v1

Atomic_dec(v)

*v中减去1

Atomic_dec_and_test(v)

*v中减去1,如果结果非空就返回1;否则返回0

Atomic_inc_and_test_greater_zero(v)

*v1,如果结果为正就返回1;否则就返回0

Atomic_clear_mask(mask,addr)

清除由mask所指定的addr中的所有位。

Atomic_set_mask(mask,addr)

设置由mask所指定的addr中的所有位。

 

4.8.3 自旋锁、读写自旋锁和大读者自旋锁

   Linux开发的早期,开发者就面临这样的问题,即不同类型的上下文(用户进程对中断)如何访问共享的数据,以及如何访问来自多个CPU同一上下文的不同实例。

   Linux内核中,临界区的代码或者是由进程上下文来执行,或者是由中断上下文来执行。在单CPU上,可以用cli/sti指令来保护临界区的使用,例如:

 

unsigned long flags;

 

save_flags(flags);

cli();

/* critical code */

restore_flags(flags);

 

    但是,在SMP上,这种方法明显是没有用的,因为同一段代码序列可能由另一个进程同时执行,cli()仅仅能单独地为每个CPU上的中断上下文提供对竟争资源的保护,它无法对运行在不同CPU上的上下文提供对竟争资源的访问。因此,必须用到自旋锁。

   所谓自旋锁,就是当一个进程发现锁被另一个进程锁着时,它就不停地“旋转”,不断执行一个指令的循环直到锁打开。自旋锁只对SMP有用,对单CPU没有意义。

 

4.9 本章小节

通过本章的介绍,读者应该对进程有一个全方位的认识:

进程是由正文段(text)、用户数据段(user segment)以及系统数据段(system segment)共同组成的一个执行环境。

Linux中用task_struct结构来描述进程,也就是说,有关进程的所有信息都存储在这个数据结构中,或者说,Linux中的进程与task_struct结构是同意词,在英文描述中,有时把进程(process 和线程(thread)混在一起使用,但并不是说,进程与线程有同样的含义,只不过描述线程的数据结构也是task_structtask_struct就是一般教科书上所讲的进程控制块.

相对独立的内容为进程的状态,在此再次给与概述:

TASK_RUNNING:也就是通常所说的就绪(ready)状态

TASK_INTERRUPTIBLE:等待一个信号或一个资源 (睡眠状态)

TASK_UNINTERRUPTIBLE, 等待一个资源 (睡眠状态), 处于某个等待队列中。

TASK_ZOMBIE, 没有父进程的子进程

TASK_STOPPED, 正在被调试的任务。

task_struct结构与内核栈存放在一起,8K的空间。

当前进程就是在某个CPU上正在运行的进程,Linux中用宏current来描述,也可以把curennt当作一个全局变量来用。

为了把内核中的所有进程组织起来,Linux提供了几种组织方式,其中哈希表和双向循环链表方式是针对系统中的所有进程(包括内核线程),而运行队列和等待队列是把处于同一状态的进程组织起来。

Linux2.4中引入一种通用链表list_head,这是面向对象思想在C中的具体实现,在内核中其他使用链表的地方都引用了这种基类型。

进程的权能和内核的同步我们仅仅做了简单介绍,因为进程管理会涉及到这些内容,但它们不是进程管理的核心内容,引入这些内容仅仅是为了让读者在阅读源代码时扫除一些障碍。

进程的创建及执行将在第六章的最后一节进行讨论。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值