从一个进程诞生到退出的时间里,内核的进程管理子系统执行了各种操作,从进程创建、分配CPU时间、事件通知到进程终止时销毁进程。
除了地址空间之外,一个进程在内存中还被分配了一个称为进程描述符的数据结构,内核用它来识别、管理和调度该进程。图1-4描述了内核中的进程地址空间及其进程描述符。
图1-4
在Linux中,一个进程描述符是<linux/sched.h>中定义的struct task_struct类型的一个实例,它是核心数据结构之一,包含一个进程所拥有的所有属性、标识的详细信息和资源分配条目。查看struct task_struct就像是窥探内核在管理和调度进程时所看到或所使用的内容。
由于任务结构体包含一系列广泛的数据元素,这些元素与不同的内核子系统的功能相关,因此在本章中我们将单独探讨所有元素的目的和范围。我们将介绍一些与进程管理相关的重要元素。
进程属性定义了一个进程的所有关键特征和基本特征。这些元素包含进程的状态和标识以及其他重要的键值。
一个进程从其产生之时起直至退出就一直处于不同的状态中,称为进程状态——它们定义了进程的当前状态。
● TASK_RUNNING (0):任务正在执行或在调度器运行队列中争抢CPU。
● TASK_INTERRUPTIBLE (1):任务处于可中断的等待状态;它仍然处于等待状态,直到所等待的条件变为真,例如互斥锁可用、I/O准备好的设备、睡眠时间超时,或者是一个专属的唤醒调用。在这个等待状态中,为进程生成的任何信号都被传递,使得等待条件被满足前唤醒进程。
● TASK_KILLABLE :这与 TASK_INTERRUPTIBLE 类似,不同之处在于中断只能发生在致命信号上,这使它成为 TASK_INTERRUPTIBLE 以外更好的选择。
● TASK_UNINTERRUPTIBLE (2):任务处于不可中断的等待状态,类似于 TASK_ INTERRUPTIBLE ,但是产生信号给这种睡眠进程不会导致其被唤醒。当它正在等待的事件发生时,进程才转换为 TASK_RUNNING 状态。该进程状态很少使用。
● TASK_STOPPED (4):该任务已经收到停止(STOP)信号。在接收到继续信号(SIGCONT)后,它会回到运行状态。
● TASK_TRACED (8):当一个进程可能正在被一个调试器仔细检查,便可认为它处于跟踪状态。
● EXIT_ZOMBIE (32):该进程已经被终止,但它的资源尚未回收。
● EXIT_DEAD (16):父进程使用wait方法收集子进程的退出状态后,子进程将终止,并且释放它所持有的所有资源。
图1-5描述了进程状态。
图1-5
该字段保存了进程唯一的标识符,称为 PID。Linux中的PID是pid_t(整数)类型。虽然PID是一个整数,但通过/proc/sys/kernel/pid_max接口指定的默认最大值只有32 768。该文件中的值可以设置为任何值,最高可达2 22 (PID_MAX_LIMIT,约为400万)。
为了管理 PID,内核使用了位图。该位图允许内核跟踪PID的使用情况,并且可以为新进程分配唯一的PID。每个PID都是由PID位图中的一个位来标识的;PID的值是根据其对应位的位置来确定的。在位图中,值为1的位表示正在使用相应的PID,值为0的位表示空闲的PID。每当内核需要分配一个唯一的PID时,它就会查找第一个未被设置的位并将其设置为1,相反地,释放一个PID时,它会将相应的位从1设置为0。
该字段保存了线程组id。为了便于理解,假设创建了一个新进程,它的PID和TGID是相同的,因为进程恰好是唯一的线程。当进程产生一个新的线程时,新的子进程将获得唯一的PID,但是继承了父线程的TGID,因为它属于同一个线程组。TGID主要用于支持多线程进程。我们将在本章后面的线程部分深入了解。
该字段保存了处理器特定的状态信息,并且它是任务结构体的关键元素。本章后文会包含有关thread_info的重要细节。
该标志字段记录了进程相应的各种属性。该字段中的每一位对应于一个进程生命周期中的各个阶段。每个进程标志定义在<linux/sched.h>中。
#define PF_EXITING /* getting shut down */ #define PF_EXITPIDONE /* pi exit done on shut down */ #define PF_VCPU /* I'm a virtual CPU */ #define PF_WQ_WORKER /* I'm a workqueue worker */ #define PF_FORKNOEXEC /* forked but didn't exec */ #define PF_MCE_PROCESS /* process policy on mce errors */ #define PF_SUPERPRIV /* used super-user privileges */ #define PF_DUMPCORE /* dumped core */ #define PF_SIGNALED /* killed by a signal */ #define PF_MEMALLOC /* Allocating memory */ #define PF_NPROC_EXCEEDED /* set_user noticed that RLIMIT_NPROC was exceeded */ #define PF_USED_MATH /* if unset the fpu must be initialized before use */ #define PF_USED_ASYNC /* used async_schedule*(), used by module init */ #define PF_NOFREEZE /* this thread should not be frozen */ #define PF_FROZEN /* frozen for system suspend */ #define PF_FSTRANS /* inside a filesystem transaction */ #define PF_KSWAPD /* I am kswapd */ #define PF_MEMALLOC_NOIO0 /* Allocating memory without IO involved */ #define PF_LESS_THROTTLE /* Throttle me less: I clean memory */ #define PF_KTHREAD /* I am a kernel thread */ #define PF_RANDOMIZE /* randomize virtual address space */ #define PF_SWAPWRITE /* Allowed to write to swap */ #define PF_NO_SETAFFINITY /* Userland is not allowed to meddle with cpus_allowed */ #define PF_MCE_EARLY /* Early kill for mce process policy */ #define PF_MUTEX_TESTER /* Thread belongs to the rt mutex tester */ #define PF_FREEZER_SKIP /* Freezer should not count it as freezable */ #define PF_SUSPEND_TASK /* this thread called freeze_processes and should not be frozen */
这些字段保存了任务的退出值和导致终止的信号的详细信息。这些字段将由父进程在子进程终止时通过wait()访问。
该字段保存了用于启动进程的二进制可执行文件的名称。
当使用ptrace()系统调用使进程转为跟踪模式时,将启用并设置该字段。
每个进程都可以与父进程关联,并建立父子关系。同样,由同一进程产生的多个进程被称为兄弟进程。这些字段确定当前进程与另一个进程的关系。
这些是指向父任务结构体的指针。对于正常的进程,这两个指针都指向同一个task_struct。它们的区别仅在于使用posix线程实现的多线程进程。对于这种情况,real_parent指向父线程任务结构体,parent指向收到SIGCHLD信号的进程任务结构体。
这是指向子任务结构体链表的指针。
这是一个指向兄弟任务结构体链表的指针。
这个指针指向进程组组长的任务结构体。
所有相互竞争的进程都必须拥有公平的CPU时间,这就要求基于时间片和进程优先级来调度。以下这些属性包含了调度器所需的必要信息,以帮助确定哪个进程在竞争时获得优先权。
prio帮助确定调度进程的优先级。如果进程被分配了实时调度策略,则此字段保存了进程的静态优先级,范围为1~99(由sched_setscheduler()指定)。对于正常的进程,这个字段保存了由nice值得来的动态优先级。
每个任务都属于调度实体(任务组),因为调度是在每个实体级别上完成的。se用于所有正常进程,rt用于实时进程,dl用于截止期进程。我们将在下一章讨论关于调度的这些属性的更多细节。
该字段保存了和进程调度策略相关的信息,这有助于确定进程的优先级。
该字段指定了进程的CPU掩码。也就是说,在多处理器系统中,进程允许在哪个CPU上进行调度。
该字段用于指定实时调度策略的进程优先级。但对于非实时进程,该字段未被使用。
内核施加资源限制以确保在相互竞争的进程中公平分配系统资源。这些限制保证了任意一个进程都不会独占所有的资源。有16种不同类型的资源限制,task structure指向一个struct rlimit类型的数组,其中每个偏移量包含了一个特定资源的当前值和最大值。
/*include/uapi/linux/resource.h*/ struct rlimit { __kernel_ulong_t rlim_cur; __kernel_ulong_t rlim_max; };
这些限制在include/uapi/asm-generic/resource.h中进行了指定。
#define RLIMIT_CPU 0 /* CPU time in sec */ #define RLIMIT_FSIZE 1 /* Maximum filesize */ #define RLIMIT_DATA 2 /* max data size */ #define RLIMIT_STACK 3 /* max stack size */ #define RLIMIT_CORE 4 /* max core file size */ #ifndef RLIMIT_RSS # define RLIMIT_RSS 5 /* max resident set size */ #endif #ifndef RLIMIT_NPROC # define RLIMIT_NPROC 6 /* max number of processes */ #endif #ifndef RLIMIT_NOFILE # define RLIMIT_NOFILE 7 /* max number of open files */ #endif #ifndef RLIMIT_MEMLOCK # define RLIMIT_MEMLOCK 8 /* max locked-in-memory address space */ #endif #ifndef RLIMIT_AS # define RLIMIT_AS 9 /* address space limit */ #endif #define RLIMIT_LOCKS 10 /* maximum file locks held */ #define RLIMIT_SIGPENDING 11 /* max number of pending signals */ #define RLIMIT_MSGQUEUE 12 /* maximum bytes in POSIX mqueues */ #define RLIMIT_NICE 13 /* max nice prio allowed to raise to 0-39 for nice level 19 .. -20 */ #define RLIMIT_RTPRIO 14 /* maximum realtime priority */ #define RLIMIT_RTTIME 15 /* timeout for RT tasks in us */ #define RLIM_NLIMITS 16
在进程的生命周期中,它可以访问各种资源文件来完成其任务。这会导致进程打开、关闭、读取和写入这些文件。而系统又必须跟踪这些行为;文件描述符元素可以帮助系统了解进程操作了哪些文件。
文件系统信息存储在该字段中。
文件描述符表保存了一些指针,这些指针指向进程为了执行各种操作而打开的所有文件。而files字段保存了一个指向该文件描述符表的指针。
对于要处理信号的进程,任务结构体中有各种元素,而这些元素决定着信号必须如何处理。
这是struct signal_struct类型的元素,它保存了与进程相关的所有信号的信息。
这是struct sighand_struct类型的元素,它保存了与进程相关的所有信号的处理函数。
这些元素标识了当前被进程屏蔽或阻塞的信号。
这是struct sigpending 类型的,它用来标识已经生成但尚未传递的信号。
该字段保存了一个指向备用堆栈的指针,它有助于信号处理。
该字段表示用于信号处理的备用堆栈的大小。