天天看点

深入分析Linux内核源代码4-进程描述(2)

每天十五分钟,熟读一个技术点,水滴石穿,一切只为渴望更优秀的你!

————零声学院

4.4 task_struct 结构在内存中的存放

task_struct 结构在内存的存放与内核栈是分不开的,因此,首先讨论内核栈。

4.4.1 进程内核栈

每个进程都有自己的内核栈。当进程从用户态进入内核态时,CPU 就自动地设置该进程

的内核栈,也就是说,CPU 从任务状态段 TSS 中装入内核栈指针 esp(参见下一章的进程切换

一节)。

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

在 Intel 系统中,栈起始于末端,并朝这个内存区开始的方向增长。从用户态刚切换到

内核态以后,进程的内核栈总是空的,因此,esp 寄存器直接指向这个内存区的顶端在图 4.2

中,从用户态切换到内核态后,esp 寄存器包含的地址为 0x018fc00。进程描述符存放在从

0x015fa00 开始的地址。只要把数据写进栈中,esp 的值就递减。

深入分析Linux内核源代码4-进程描述(2)

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

union task_union { 
 struct task_struct task; 
 unsigned long stack[2408]; 
}; 
           

从这个结构可以看出,内核栈占 8KB 的内存区。实际上,进程的 task_struct 结构所占

的内存是由内核动态分配的,更确切地说,内核根本不给 task_struct 分配内存,而仅仅给

内核栈分配 8KB 的内存,并把其中的一部分给 task_struct 使用。

task_struct 结构大约占 1K 字节左右,其具体数字与内核版本有关,因为不同的版本其

域稍有不同。因此,内核栈的大小不能超过 7KB,否则,内核栈会覆盖 task_struct 结构,

从而导致内核崩溃。不过,7KB 大小对内核栈已足够。

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

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

task_struct = (struct task_struct *) STACK_POINTER & 0xffffe000

• 避免在创建进程时动态分配额外的内存。

• task_struct 结构的起始地址总是开始于页大小(PAGE_SIZE)的边界。

4.4.2 当前进程(current 宏)

当一个进程在某个 CPU 上正在执行时,内核如何获得指向它的 task_struct 的指针?上

面所提到的存储方式为达到这一目的提供了方便。在 linux/include/ i386/current.h 中

定义了 current 宏,这是一段与体系结构相关的代码:

tatic inline struct task_struct * get_current(void) 
{ 
struct task_struct *current; 
__asm__("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL)); 
return current; 
}
           

实际上,这段代码相当于如下一组汇编指令(设 p 是指向当前进程 task_struc 结构的

指针):

movl $0xffffe000, %ecx

andl %esp, %ecx

movl %ecx, p

换句话说,仅仅只需检查栈指针的值,而根本无需存取内存,内核就可以导出

task_struct 结构的地址。

在本书的描述中,会经常出现 current 宏,在内核代码中也随处可见,可以把它看作全

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

另外,在 include/ i386/processor.h 中定义了两个函数 free_task_struct( )和

alloc_task_struct( ),前一个函数释放 8KB 的 task_union 内存区,而后一个函数分配 8KB

的 task_union 内存区。

4.5 进程组织方式

在 Linux 中,可以把进程分为用户任务和内核线程,不管是哪一种进程,它们都有自己

的 task_struct。在 2.4 版中,系统拥有的进程数可能达到数千乃至上万个,尤其对于企业

级应用(如数据库应用及网络服务器)更是如此。为了对系统中的很多进程及处于不同状态

的进程进行管理,Linux 采用了如下几种组织方式。

4.5.1 哈希表

哈希表是进行快速查找的一种有效的组织方式。Linux 在进程中引入的哈希表叫做

pidhash,在 include/linux/sched.h 中定义如下:

#define PIDHASH_SZ (4096 >> 2)

extern struct task_struct *pidhash[PIDHASH_SZ];

#define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))

其中,PIDHASH_SZ 为表中元素的个数,表中的元素是指向 task_struct 结构的指针。

pid_hashfn 为哈希函数,把进程的 PID 转换为表的索引。通过这个函数,可以把进程的 PID

均匀地散列在它们的域(0 到 PID_MAX-1)中。

在数据结构课程中我们已经了解到,哈希函数并不总能确保 PID 与表的索引一一对应,

两个不同的 PID 散列到相同的索引称为冲突。

Linux 利用链地址法来处理冲突的 PID:也就是说,每一表项是由冲突的 PID 组成的双

向链表,这种链表是由 task_struct 结构中的 pidhash_next 和 pidhash_pprev 域实现的,

同一链表中 pid 的大小由小到大排列。如图 4.3 所示。

哈希表 pidhash 中插入和删除一个进程时可以调用 hash_ pid( ) 和 unhash_ pid( )

函数。对于一个给定的 pid,可以通过 find_task_by_pid()函数快速地找到对应的进程:

static inline struct task_struct *find_task_by_pid(int pid) 
{ 
struct task_struct *p, **htable = &pidhash[pid_hashfn(pid)]; 
 for(p = *htable; p && p->pid != pid; p = p->pidhash_next) 
 ; 
 return p; 
}
           
深入分析Linux内核源代码4-进程描述(2)

4.5.2 双向循环链表

哈希表的主要作用是根据进程的 pid 可以快速地找到对应的进程,但它没有反映进程创

建的顺序,也无法反映进程之间的亲属关系,因此引入双向循环链表。每个进程 task_struct

结构中的 prev_task 和 next_task 域用来实现这种链表,如图 4.4 所示。

深入分析Linux内核源代码4-进程描述(2)

宏 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,它对应的是进程 0(pid 为 0),

也就是所谓的空进程,它是所有进程的祖先。这个宏把进程之间的亲属关系也链接起来。另

外,还有一个宏 for_each_task():

#define for_each_task(p) \

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

这个宏是循环控制语句。注意 init_task 的作用,因为空进程是一个永远不存在的进程,

因此用它做链表的头和尾是安全的。

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

开锁。

4.5.3 运行队列

当内核要寻找一个新的进程在 CPU 上运行时,必须只考虑处于可运行状态的进程(即在

TASK_RUNNING 状态的进程),因为扫描整个进程链表是相当低效的,所以引入了可运行状态

进程的双向循环链表,也叫运行队列(runqueue)。

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

所示。

深入分析Linux内核源代码4-进程描述(2)

4.5.4 进程的运行队列链表

该队列通过 task_struct 结构中的两个指针 run_list 链表来维持。队列的标志有两个:

一个是“空进程”idle_task,一个是队列的长度。

有两个特殊的进程永远在运行队列中待着:当前进程和空进程。前面我们讨论过,当前

进程就是由 cureent 指针所指向的进程,也就是当前运行着的进程,但是请注意,current

指针在调度过程中(调度程序执行时)是没有意义的,为什么这么说呢?调度前,当前进程

正在运行,当出现某种调度时机引发了进程调度,先前运行着的进程处于什么状态是不可知

的,多数情况下处于等待状态,所以这时候 current 是没有意义的,直到调度程序选定某个

进程投入运行后,current 才真正指向了当前运行进程;空进程是个比较特殊的进程,只有

系统中没有进程可运行时它才会被执行,Linux 将它看作运行队列的头,当调度程序遍历运

行队列,是从 idle_task 开始、至 idle_task 结束的,在调度程序运行过程中,允许队列中

加入新出现的可运行进程,新出现的可运行进程插入到队尾,这样的好处是不会影响到调度

程序所要遍历的队列成员,可见,idle_task 是运行队列很重要的标志。

另一个重要标志是队列长度,也就是系统中处于可运行状态(TASK_RUNNING)的进程数

目,用全局整型变量 nr_running 表示,在/kernel/fork.c 中定义如下:

int nr_running=1;

若 nr_running 为 0,就表示队列中只有空进程。在这里要说明一下:若 nr_running 为

0,则系统中的当前进程和空进程就是同一个进程。但是 Linux 会充分利用 CPU 而尽量避免出

现这种情况。

4.5.5 等待队列

在 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 定义了 5 个宏: 
#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) 
#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) 
           

前 3 个宏都是初始化一个空的链表,但用法不同,LIST_HEAD_INIT()在声明时使用,用

来初始化结构元素,第 2 个宏用在静态变量初始化的声明中,而第 3 个宏用在函数内部。

其中,最难理解的宏为 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 ;

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

struct __wait_queue_head { 
 wq_lock_t lock; 
 struct list_head task_list; 
}; 
           

typedef struct __wait_queue_head wait_queue_head_t;

在这两个数据结构的定义中,都涉及到类型为 list_head 的链表,这与 2.2 版定义是不

同的,在 2.2 版中的定义为:

struct wait_queue { 
 struct task_struct * task; 
 struct wait_queue * next ; 
} ; 
typedef struct wait_queue wait_queue_t ; 
typedef struct wait_queue *wait_queue_head_t ; 
           

这里要特别强调的是,2.4 版中对等待队列的操作函数和宏比 2.2 版丰富了,而在你编

写设备驱动程序时会用到这些函数和宏,因此,要注意 2.2 到 2.4 函数的移植问题。下面给

出 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++中

的基类型,这也是对 2.2 版的极大改进。

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

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 从等待队列中删除 */

}

这个函数把 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 宏,

也就是说,如果你要在内核级进行编程,只需调用其中的一个宏。例如一个简单的实时时钟

(RTC)中断程序如下:

static DECLARE_WAIT_QUEUE_HEAD(rtc_wait); /*初始化等待队列首部*/ 
void rtc_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{ 
 spin_lock(&rtc_lock); 
 rtc_irq_data = CMOS_READ(RTC_INTR_FLAGS); 
 spin_unlock(&rtc_lock); 
 wake_up_interruptible(&rtc_wait); 
} 
           

这个中断处理程序通过从实时时钟的 I/O 端口(CMOS_READ 宏产生一对 outb/inb)读取

数据,然后唤醒在 rtc_wait 等待队列上睡眠的任务。

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)”表示一进程所具有的权力。一种权能仅仅是一个标志,

它表明是否允许进程执行一个特定的操作或一组特定的操作。这个模型不同于传统的“超级

用户对普通用户”模型,在后一种模型中,一个进程要么能做任何事情,要么什么也不能做,

这取决于它的有效 UID。也就是说,超级用户与普通用户的划分过于笼统。如表 4.13 给出了

在 Linux 内核中已定义的权能。

深入分析Linux内核源代码4-进程描述(2)

任何时候,每个进程只需要有限种权能,这是其主要优势。因此,即使一位有恶意的用

户使用有潜在错误程序,他也只能非法地执行有限个操作类型。

例如,假定一个有潜在错误的程序只有 CAP_SYS_TIME 权能。在这种情况下,利用其错

误的恶意用户只能在非法地改变实时时钟和系统时钟方面获得成功。他并不能执行其他任何

特权的操作。

4.8 内核同步

内核中的很多操作在执行的过程中都不允许受到打扰,最典型的例子就是对队列的操

作。如果两个进程都要将一个数据结构链入到同一个队列的尾部,要是在第 1 个进程完成了

一半的时候发生了调度,让第 2 个进程插了进来,就可能造成混乱。类似的干扰可能来自某

个中断服务程序或 bh 函数。在多处理机 SMP 结构的系统中,这种干扰还有可能来自另一个处

理器。这种干扰本质上表项为对临界资源(如队列)的互斥使用。下面介绍几种避免这种干

扰的同步方式。

4.8.1 信号量

进程间对共享资源的互斥访问是通过“信号量”机制来实现的。信号量机制是操作系统

教科书中比较重要的内容之一。Linux 内核中提供了两个函数 down()和 up(),分别对应于

操作系统教科书中的 P、V 操作。

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

struct semaphore { 
 atomic_t count; 
 int sleepers; 
 wait_queue_head_t wait; 
 #if WAITQUEUE_DEBUG 
 long __magic; 
 #endif 
 }; 
           

其中的 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;

深入分析Linux内核源代码4-进程描述(2)

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 没有意义。

有 3 种类型的自旋锁:基本的、读写以及大读者自旋锁。读写自旋锁适用于“多个读者

少数写者”的场合,例如,有多个读者仅有一个写者,或者没有读者只有一个写者。大读者

自旋锁是读写自旋锁的一种,但更照顾读者。大读者自旋锁现在主要用在 Sparc64 和网络系

统中。

本章对进程进行了全面描述,但并不是对每个部分都进行了深入描述,因为在整个操作

系统中,进程处于核心位置,因此,内核的其他部分(如文件、内存、进程间的通信等)都

与进程有密切的联系,相关内容只能在后续的章节中涉及到。通过本章的介绍,读者应该对

进程有一个全方位的认识:

(1)进程是由正文段(Text)、用户数据段(User Segment)以及系统数据段(System

Segment)共同组成的一个执行环境。

(2)Linux 中用 task_struct 结构来描述进程,也就是说,有关进程的所有信息都存储

在这个数据结构中,或者说,Linux 中的进程与 task_struct 结构是同意词,在英文描述中,

有时把进程(Process)和线程(Thread)混在一起使用,但并不是说,进程与线程有同样的

含义,只不过描述线程的数据结构也是 task_struct。task_struct 就是一般教科书上所讲的

进程控制块.。

(3)本章对 task_struct 结构中存放的信息进行了分类讨论,但并不要求在此能掌握所

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

• TASK_RUNNING:也就是通常所说的就绪(Ready)状态。

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

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

• TASK_ZOMBIE:没有父进程的子进程。

• TASK_STOPPED:正在被调试的任务。

(4)task_struct 结构与内核栈存放在一起,占 8KB 的空间。

(5)当前进程就是在某个 CPU 上正在运行的进程,Linux 中用宏 current 来描述,也可

以把 curennt 当作一个全局变量来用。

(6)为了把内核中的所有进程组织起来,Linux 提供了几种组织方式,其中哈希表和双

向循环链表方式是针对系统中的所有进程(包括内核线程),而运行队列和等待队列是把处

于同一状态的进程组织起来。

(7)Linux 2.4 中引入一种通用链表 list_head,这是面向对象思想在 C 中的具体实现,

在内核中其他使用链表的地方都引用了这种基类型。

(8)进程的权能和内核的同步我们仅仅做了简单介绍,因为进程管理会涉及到这些内容,

但它们不是进程管理的核心内容,引入这些内容仅仅是为了让读者在阅读源代码时扫除一些

障碍。

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

每日分享15分钟技术摘要选读,关注一波,一起保持学习动力!

深入分析Linux内核源代码4-进程描述(2)

继续阅读