2017年10月新番动漫:Linux2.4内核说明文档(进程与中断管理篇)

来源:百度文库 编辑:偶看新闻 时间:2024/04/30 14:03:42
Linux2.4内核说明文档(进程与中断管理篇)        作者:wantin6  时间:2006-08-08 23:34[list=]

本文档是《Linux2.4 内核说明文档》中的第二部分。以下是整个文档大致目录:

1,启动 ([url=http://bbs.chinaunix.net/forum/viewtopic.php?t=557946]http://bbs.chinaunix.net/forum/viewtopic.php?t=557946[/url] )

2,进程和中断管理

3,虚拟文件系统

4,Linux 页缓冲

5,IPC机制



本篇文档的目录为:

2.1.        Tack结构和进程表       

2.2.        创建和中止任务与内核线程       

2.3.        调度程序       

2.4.        Linux执行链表       

2.5.        等待队列       

2.6.        内核时钟       

2.7.        Bottom Halves       

2.8.        任务队列       

2.9.        I386体系中系统调用实现       

2.10.        原子操作       

2.11.        旋转锁、读写旋转锁和Big-Reader旋转锁

2.12.        信号灯和读写信号灯       

2.13.        装载模块的内核支持



一下是正文:

2.        进程和中断管理



2.1.        Tack结构和进程表

  linux下的每个进程都是动态分配一个task_struct结构,整个系统可以创建的最大进程数仅由当前可用物理内存总数限制,并且等于(见kernel/fork.c:fork_init()函数):

  

   /*

    * The default maximum number of threads is set to a safe

    * value: the thread structures can take up at most half

    * of memory.

    */

    max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 2;

   

    这个式子在IA32体系结构上主要意味着最大数为物理内存页数/4,例如:在一个512M内存机器上你可以创建32K线程。这对于旧版本(2.2或者更老)内核的4K限制是一个可观的改进,而且这可以在运行时使用系统调用sysctl(2)修改KERN_MAX_THREADS,或者简单使用procfs系统接口来调整。



# cat /proc/sys/kernel/threads.max

32764

# echo 100000 >; /proc/sys/kernel/threads.max

# cat /proc/sys/kernel/threads.max

100000

# gdb .q vmlinux /proc/kcore

Core was generated by `BOOT_IMAGE=240ac18 ro root=306 video=matrox:vesa:0x118'.

#0 0x0 in ?? ()

(gdb) p max_threads

$1 = 100000



  Linux系统上进程的关联表现为一个以下两个方式链接的task_struct结构的集合:

1)        以pid为键值的hash表。

2)        通过p->;next_task和p->;prev_task指针连接的双向链表。

这个hash表名为pidhash,并在include/linux/sched.h中定义:



/* PID hashing. (shouldnt this be dynamic?) */

#define PIDHASH_SZ (4096 >;>; 2)

extern struct task_struct *pidhash[PIDHASH_SZ];

#define pid_hashfn(x) ((((x) >;>; ^ (x)) & (PIDHASH_SZ . 1))



   所有的任务以他们的pid为键值存放到hash表中,并假定均匀地从(0 to PID_MAX-1)分布。这个hash表用来通过指定的pid快速的找到task结构,搜索函数find_task_pid()定义在include/linux/sched.h中:



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;

}



  在每个hash链上的所有任务都通过p->;pidhash_next和p->;pidhash_pprev连接起来,这在hash_pid函数和unhash_pid函数将指定进程插入hash表或者移出hash表时使用。所有的操作都受到tasklist_lock写同步锁保护。

而双向链表则为系统遍历所有的任务提供了方便,这个操作由定义在include/linux/sched.h中的for_each_tack()宏来实现。



#define for_each_task(p) \

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



for_each_task()函数的使用者必须使用tasklist_lock读同步锁。注意for_each_task()函数采用init_task标识链表的起点,这样才是安全的,因为空任务(pid = 0) 是不在链表里的。进程hash表和进程链表的修改操作,特别是fork函数,exit函数和ptrace函数,必须调用tasklist_lock写同步锁。更有趣的是,所有的写操作还必须屏蔽当前CPU的中断,这个原因是显而易见的:send_sigio函数遍历了进程表,这样需要调用tasklist_lock读同步锁,并且该函数是kill_fasync函数在中断环境下调用的。

现在我们已经知道task_struct结构是怎样链接到一起的,现在让我们分析一下task_struct结构的成员。这些成员是UNIX系统的proc结构和user结构松散组合到一起的。

其他UNIX版本总是将进程状态信息作为单独一部分常驻内存,其他部分则作为进程运行时所需信息,如此简陋的设计仅仅因为内存时非常宝贵的资源。现代操作系统(如Linux或者FreeBSD)并不做如此区分,而是在内核常驻内存的数据结构中维护进程状态。

include/linux/sched.h中定义了task_struct结构,并且通常大写为1680字节,状态宏也定义同一个头文件中。



volatile long state; /* .1 unrunnable, 0 runnable, >;0 stopped */

#define TASK_RUNNING 0

#define TASK_INTERRUPTIBLE 1

#define TASK_UNINTERRUPTIBLE 2

#define TASK_ZOMBIE 4

#define TASK_STOPPED 8

#define TASK_EXCLUSIVE 32



为什么TASK_EXCLUSIVE宏定义为32而不是16呢?这是由于16被TASK_SWAPPING使用了,并且后来在移出TASK_SWAPPING时没有把TASK_EXCLUSIVE的值上调。

可变量p->;state的定义意味着它自身可以被中断处理者异步修改。

1)        TASK_RUNNING:含义是假定任务已经处于运行队列中。至于不是已经处于运行队列的原因是由于将一个任务标识为TASK_RUNNING和将该任务移动到运行队列不是一个原子操作。从运行队列角度考虑,操作时需要保持runqueue_lock读同步锁。如果这样操作,你将发现在运行队列的每个任务都处于TASK_RUNNING状态。然后,反过来却不一定。同样地,驱动程序可以标识他们自身状态为TASK_INTERRUPTIBLE,然后调用schedule()函数,这个函数将从运行队列移出它自己(除非当时有一个导致它滞留在运行队列的未处理信号)。

2)        TASK_INTERRUPTIBLE:含义是任务处于休眠状态但可以通过一个信号或者休眠中止时钟唤醒。

3)        TASK_UNINTERRUPTIBLE:含义类似于TASK_INTERRUPTIBL,但任务不能被唤醒。

4)        TASK_ZOMBIE:含义是任务已经被中止但它的状态还没被父进程获取。

5)        TASK_STOPPED:含义是由于任务控制信号或者ptrace系统调用,任务已经被停止。

6)        TASK_EXCLUSIVE:含义是这不是一个单独状态,但能够与TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE状态并存(OR操作)。这意味着当任务与其他在等待队列休眠时,它可以单独被唤醒而不需要唤醒整个等待队列的任务。

任务标记包含了关于非互相排斥的进程状态信息。



unsigned long flags; /* per process flags, defined below */

/*

* Per process flags

*/

#define PF_ALIGNWARN 0x00000001 /* Print alignment warning msgs */

/* Not implemented yet, only for 486*/

#define PF_STARTING 0x00000002 /* being created */

#define PF_EXITING 0x00000004 /* getting shut down */

#define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */

#define PF_SUPERPRIV 0x00000100 /* used super.user privileges */

#define PF_DUMPCORE 0x00000200 /* dumped core */

#define PF_SIGNALED 0x00000400 /* killed by a signal */

#define PF_MEMALLOC 0x00000800 /* Allocating memory */

#define PF_VFORK 0x00001000 /* Wake up parent in mm_release */

#define PF_USEDFPU 0x00100000 /* task used FPU this quantum (SMP) */



p->;has_cpu, p->;processor, p->;counter, p->;priority, p->;policy and p->;rt_priority字段和调度程序关联,并将在后面描述。

p->;mm 和p->;active_mm字段分别指向mm_struct结构描述的进程地址空间和有效地址空间(如果这个进程不是内核进程的话)。这使得当任务被调度离开时TLB能够在地址空间自由切换。所以,如果当前正在执行内核任务(没有p->;mm),则它的next->;active_mm将被设置为已经被调度离开的任务的prev->;active_mm,如果pre-mm != NULL,则这个地址将和prev->;mm相同。如果CLONE_VM标识传递到了clone系统调用或者依靠vfork系统调用,则地址空间可以在任务之间共享。

p->;exec_domain和p->;personality字段与任务的特性相关,也就是为了模仿UNIX特性的唯一系统调用。

p->;fs字段包含了文件系统信息,在linux下有三个方面的含义:

1)        root目录实体结构和挂载点;

2)        预备的root目录实体和挂载点;

3)        当前工作目录实体和挂载点;

这个结构同样包含了一个引用计数,因为当进行带CLONE_FS标识的clone系统调用时,它是共享的。

p->;files字段包含了文件句柄表,这在进行带CLONE_FILES标识clone系统调用时也是多任务共享的。

p->;sig字段包含了信号处理函数入口,以CLONE_SIGHAND参数执行clone操作后页可以在进程间共享。





__________________________________

雪,是我的印记;

风,是我的侍从.

请,别问我是谁;

我,其实不是谁.

我只是飘摇的飞雪,阳光下融在风里的孤雕.





  





2.2.        创建和中止任务与内核线程

不同的操作系统书籍,从一个“正在执行的程序的实例”到“由clone或者fork系统调用产生的任务”等不同方式定义了“进程”。在linux下,共有三种类型程序:

        空线程;

        内核线程;

        用户任务;

空线程在为第一个CPU引导时创建,然后依靠定义在arch/i386/kernel/smpboot.c的fork_by_hand()函数手工为每个CPU创建这个线程。所有的空线程共享一个init_task结构,但都拥有各自的存放在CPU队列里的init_tss表示的TSS结构。他们以CLONE_PID方式clone,PID都为零,其他任务都不能共享这个PID。

内核模式下,kernel_thread函数调用clone系统调用创建了内核线程。内核线程通常没有用户地址空间,也就是p->;mm=NULL,因为他们明确通过daemonize()函数执行exit_mm()函数。内核线程通常可以直接操作内核地址空间,并被分配低范围的pid号。当在处理器模式下运行时意味着内核线程将享用所有的I/O特权并不能被调用程序预清空。

用户任务通过clone或者fork系统调用创建,他们都在内部调用了kernel/fork.c的do_fork()函数。

让我们分析一下当用户进程调用fork系统调用时发生了什么。虽然fork操作在传递用户堆栈和寄存器时依赖于体系架构,但在下面真实执行这个操作的do_fork()函数确实简洁的,并位于kernel/fork.c文件。

以下步骤将被执行:

1)        本地变量被设置为-ENOMEM,当fork创建一个新任务结构失败时将作为错误代码返回。

2)        .如果CLONE_PID标识被设置,则返回-EPERM错误,除非调用者是空线程。普通用户线程clone时不能传递CLONE_PID标识并期待操作成功。SIFCHLDclone标识,对于fork来说,它被认为是不相关的,仅在sys_clone调用do_fork时才被认为是相关的。

3)        初始化current->;vfork_sem。它将在sys_vfork函数为了休眠父进程直到子进程执行mm_release函数时使用,就像执行其他程序或者中止其他程序一样。

4)        通过alloc_task_struct()宏分配一个新的任务结构。在x86系统上,它仅是一个GFP_KERNEL优先级的gfp。这酒是为何fork系统调用可能休眠的第一个原因。如果分配失败,返回-ENOMEM错误。

5)        通过结构拷贝*p = *current,将所有当前进程结构的数据都拷贝到新进程,或许这个操作应该被memcpy替换。然后,所有不能被子进程修改的字段将被设置为正确的值。

6)        大范围的内核锁被采用以防止其他部分执行本段代码。

7)        如果父进程拥有用户资源则校验是否超出了RLIMIT_NPROC限制。如果是这样,则返回-EAGAIN错误;如果没有,则通过指定的uid将计数器p->;user->;count进程数刷新。

        如果系统所有的任务数目超过了最大线程数,返回-EAGAIN错误。

9)        如果进程是依赖预模块执行的,则增加依赖模块的引用计数。

10)        如果进程是依赖预模块二进制格式的,也增加依赖模块的引用计数。

11)        子进程被标识为“没有被执行”(p->;did_exec=0)。

12)        子进程被标识为'not.swappable' (p->;swappable = 0)。

13)        子进程被置为TASK_UNINTERRUPTIBLE状态,即p->;state = TASK_UNINTERRUPTIBLE。

14)        依照clone_flags的数值设置子进程的p->;flags,如果是简单fork,p->;flags= PF_FORKNOEXEC。

15)        通过快速算法kernel/fork.c的get_pid()函数设置子进程号p->;pid。

16)        初始化子进程其他任务结构。最后子进程结构被插入到pidhash表中,并且被唤醒。

这样任务就被创建了。停止任务有很多方式。

1)        通过exit系统调用;

2)        收到一个中止信号;

3)        被确定异常强制中止;

4)        以func == 1参数调用bdflush。

系统调用的实现函数都有sys_前缀,当他们通常仅与参数检测或者以细节方式传递信息,真正的操作是由do_**函数完成的。所以sys_exit()函数调用了do_exit来完成操作。尽管如此,内核其他部分有时也通过调用sys_exit实现堆do_exit的调用。

do_exit函数定义在kernel/exit.c中,按照以下几个步骤执行:

        获取内核全局锁;

        在最后调用一直循环的schedule()函数;

        设置任务状态为TASK_ZOMBIE;

        以current->;pdeath_signa信号通知所有的子进程;

        以等同于SIGCHLD的信号current.>;exit_signal通知父进程;

        释放fork函数分配的资源,关闭已经打开的文件;

        在采用少量FPU切换的体系中,不管硬件设备要求什么都向FPU的所有者传递一个“none”;