这里写目录标题
进程描述符task_struct
Linux 内核涉及进程和程序的所有算法都围绕一个名为 task_struct 的数据结构建立。task_struct 相对较大,在 32 位机器上,它大约有 1.7KB。task_struct 中包含的数据能完整地描述一个正在执行的进程:它打开的文件,进程的地址空间,挂起的信号,进程的状态,还要其他很多信息。
实际上,内核中大部分处理进程的代码都是直接通过 task_struct 进行的。因此,通过 current 宏
查找到当前正在运行进程的进程描述符的速度就显得尤为重要。硬件体系结构不同,该宏的实现也不同,它必须针对专门的硬件体系结构做处理。有的硬件体系结构可以拿出一个专门寄存器来存放指向当前进程 task_struct 的指针,用于加快访问速度。而有些像 x86 这样的体系结构(其寄存器并不富余),就只能在内核栈的尾端创建 thread_info 结构,通过计算偏移间接地查找 task_struct 结构。在 PPC(PowerPC,IBM 基于 RISC 的现代微处理器)上,current 宏只需把 r2 寄存器中的值返回就行了。与 x86 不一样,PPC 有足够多的寄存器。
使用current时需注意:
- current 指针在原子模式下是没有任何意义的,也是不可用的,因为相关代码和被中断的进程没有任何管理。
- current 在获得自旋锁的代码里是可用的,但禁止访问用户空间,因为这会导致调度的发生。
在调用了 do_exit()之后,尽管线程已经僵死不能组运行了,但是系统还保留了它的进程描述符。前面说过,这样做可以让系统有办法在子系统终结后仍能获得它的信息。因此,进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的 task_struct 结构才被释放。
进程描述符中的一些字段
进程标识符 PID
PID 存放在进程描述符的 pid 字段中。 PID 被顺序编号,新创建进程的 PID 通常是前一个进程的 PID 加 1 。
为了与老版本的 Unix 和 Linux 兼容,PID 的最大值默认设置为 32767(short int 短整型的最大值),在64位系统中,这个值也可以增加到高达 400 万(这受<linux/threads.h>中所定义 PID 最大值的限制)。这个最大值很重要,因为它实际上就是系统中允许同时存在的进程的最大数目。这个值越小,转一圈就越快,本来数值大的进程比数值小的进程迟运行,但这样一来就破坏了这一原则。系统管理员通过修改/proc/sys/kernel/pid_max 来提高上限。当内核使用的 PID 达到这个上限值的时候就必须循环使用已闲置的小 PID 号。
线程组 / 线程组标识符 tgid
Unix 程序员希望同一组中的线程有共同的 PID。例如,应该可以把信号发送给指定 PID 的一组线程,这个信号会作用于该组中所有的线程。
线程有两种特殊的形式:没有用户虚拟地址空间的进程称为内核线程,共享用户虚拟地址空间的进程称为用户线程。共享同一个用户虚拟地址空间的所有用户线程组成一个线程组。线程组中的主进程称为领头线程,一个线程组中所有线程使用和该线程组的领头线程(thread group leader)相同的 PID它被放在进程描述符的 tgid 字段中。getpid()系统调用返回当前进程的 tgid 值而不是 pid 值。
当调用系统调用 clone 传入标志 CLONE_THREAD 以创建新进程时,新进程和当前进程属于一个线程组。
进程组 / 进程组标识符 pgrp
独立进程可以合并成进程组(使用 setpgrp 系统调用)。该进程组成员的 task_struct 的 pgrp 属性值都是相同的,即进程组组长的 PID。进程组简化了向组的所有成员发送信号的操作,这对于各种系统程序设计应用是有用的。进程可以使用系统调用 setpid 创建或者加入一个进程组。请注意,用管道连接的进程包含在同一个进程组中。
会话 / 会话标识符 session
几个进程组可以合并成一个会话。会话中的所有进程都有同样的会话 ID,保存在 task_struct 的 session 成员中。SID可以使用 setsid 系统调用设置。当进程调用系统调用 setsid 的时候,创建一个新的会话,会话标识符是该进程的进程标识符。创建会话的进程是会话的首进程。
Linux 是多用户操作系统,用户登录时会创建一个会话,用户启动的所有进程都属于这个会话。
内存描述符mm
内核使用内存描述符结构体(struct mm_struct)表示进程的地址空间,该结构体包含了和进程地址空间有关的全部信息。
所有的 mm_struct 结构体都通过自身的 mmlist 域连接在一个双向链表中,该链表的首元素是 init_mm 内存描述符,它代表 init 进程的地址空间。另外要注意,操作该链表的时候需要使用 mmlist_lock 锁来防止并发访问。
在进程的进程描述符(task_struct)中,mm 域存放着进程使用的内存描述符,所以 current->mm 便指向当前进程的内存描述符。
线程
线程机制是现代编程技术中常用的一种抽象概念。该机制提供了在同一程序内共享内存地址空间运行的一组线程。这些线程还可以共享打开的文件和其他资源。线程机制支持并发程序设计技术(concurrent programming),在多处理器系统上,它也能保证真正的并行处理(parallelism)。
Linux 实现线程的机制非常独特。从内核的角度说,它并没有线程这个概念。Linux 把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的 task_struct,所以在内核中,它看起来就像是一个普通的进程(只是线程和其它一些进程共享某些资源,如地址空间)。
进程有两种特殊的形式:没有用户虚拟地址空间的进程称为内核线程,共享用户虚拟地址空间的进程称为用户线程。共享同一个用户虚拟地址空间的所有用户线程组成一个线程组。
用户线程
一个进程由几个用户线程组成,每个线程都代表进程的一个执行流。现在,大部分多线程应用程序都是用pthread(POSIX thread)库的标准库函数集编写的。
内核线程
内核经常需要在后台执行一些操作。这种任务可以通过内核线程(kernel thread)完成——独立运行在内核空间的标准进程。
内核线程和普通的进程间的区别在于内核线程没有独立的地址空间(实际上指向地址空间的 mm 指针被设置为NULL)。它们只在内核空间运行,从来不切换到用户空间去,即他们没有用户上下文。
内核线程和普通进程一样,可以被调度,也可以被抢占。
内核线程启动后就一直运行直到调用 do_exit()退出,或者内核其它部分调用 kthread_stop()退出,传递给kthread_stop()的参数为 kthread_create()函数返回的 task_struct 结构的地址。