一. 进程相关的概念
相关:进程(下)
1.1 进程
教科书定义: 进程是程序执行时的一个实例。可以把它看做充分描述程序已经执行到何种程度的数据结构的汇集。
Linux内核观点: 进程的目的就是担当分配系统资源的实体。
1.2 轻量级进程
1.3 线程
linux使用内核的轻量级进程实现线程
二. 进程描述符
task_struct结构体就是进程描述符,它的字段包含了与一个进程相关的所有信息。主要有如下信息:
thread_info: 进程的基本信息
mm_struct: 指向内存区描述符的指针
tty_struct: 与进程相关的tty
fs_struct: 当前目录
files_struct: 指向文件描述符的指针
signal_struct: 所接收的信号
thread_struct: CPU相关的状态信息
2.1 进程状态(state)
#define TASK_RUNNING 0 /*进程要么在CPU上执行,要么准备执行*/
#define TASK_INTERRUPTIBLE 1 /*信号可唤醒的挂起*/
#define TASK_UNINTERRUPTIBLE 2 /*信号不可唤醒的挂起,例如陷入驱动的关键步骤*/
#define TASK_STOPPED 4 /*进程被暂停,例如收到SIGSTOP、SIGTSTP信号*/
#define TASK_TRACED 8 /*跟踪暂停,例如gdb调试的程序*/
#define EXIT_ZOMBIE 16 /*进程执行终止,但是父进程还没有发布wait4()或waitpid()*/
#define EXIT_DEAD 32
2.2 标识一个进程(tgid,pid)
一般来说,能被独立调度的每个执行上下文都必须拥有它自己的进程描述符,因此,即使共享内核大部分数据结构的轻量级进程,也有它们自己的task_struct结构。
类Unix操作系统中的每一个进程都有一个进程标识符process ID(PID)。每一个线程也有自己的线程标识符(TID)。系统调用getpid(获取pid)返回tgid(领头线程pid),gettid(获取tid)返回pid。一个多线程应用的所有线程的PID都是相同的。
2.2.1 进程描述符处理
Linux把两个不同的数据结构紧凑地存放在一个单独为进程分配的存储区域内(用thread_union表示):一个是内核态的进程堆栈,另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符,这块区域的大小通常是8192个字节,并且第一个页框的起始地址是213的倍数。
2.2.2 标识当前进程
从效率的观点来看,将thread_info和内核态堆栈紧挨着的主要好处是:内核很容易从esp寄存器的值获得当前在CPU上正在运行进程的thread_info结构的地址。内核屏蔽掉esp的低13位有效位就可以获得thread_info结构的基地址。在thread_info结构体中有task_struct的指针,宏current使用此方式可以快速的获取当前进程的task_struct指针。
2.2.3 进程链表
进程链表头是init_task。进程链表把所有进程的描述符链接起来。
为了提高进程调度的效率,内核结构体prio_array_t包含了140个可运行进程(TASK_RUNNING)链表。每种进程优先级对应一个不同的链表。task_struct结构体成员run_list字段把该进程链入对应优先级的可运行链表中。task_struct的array指向结构体prio_array_t。
在多处理器系统中,每个处理器都有它自己的运行队列。
enqueue_task(p,array)函数把进程描述符插入某个运行队列的链表。
2.3 进程间的关系
程序创建的进程具有父子关系。如果一个进程创建了多个子进程,则子进程之间具有兄弟关系。进程0(swapper)和进程1是由内核创建的;进程1(init)是所有进程的祖先。
task_struct结构体中表示进程亲属关系的字段
struct task_struct *real_parent; /* real parent process (when being debugged) */
struct task_struct *parent; /* parent process */
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
task_struct结构体中表示进程非亲属关系的字段
struct task_struct *group_leader; /* P所在进程组的领头进程的描述符指针 */
struct signal_struct *signal->pgrp; /* P所在进程组的领头进程的PID */
pid_t tgid; /* P所在线程组的领头进程的PID */
struct signal_struct *signal->session; /* P的登录会员领头进程的PID */
struct list_head ptrace_children; /* 所有被debugger程序跟踪的进程P的子进程 */
struct list_head ptrace_list; /* 所有被跟踪进程的链表 */
2.3.1 pid_hash表及链表
为了快速的通过进程的pid找到相关的task_struct,内核使用了pid_hash链表头。内核有4种不同类型的PID字段:
PIDTYPE_PID, /* 进程的PID */
PIDTYPE_TGID, /* 线程组领头进程的PID */
PIDTYPE_PGID, /* 进程组领头进程的PID */
PIDTYPE_SID, /* 会话领头进程的PID */
hash表的桶大小跟RAM容量相关。task_struct的hash node的链表头在成员结构体 struct pid pids[PIDTYPE_MAX]中。内核有一组函数和宏来处理PID散列表。
2.4 如何组织进程
2.4.1 等待队列
等待队列在内核中有很多用途,尤其用在中断处理、进程同步及定时。每个等待队列都有一个等待队列头,类型为wait_queue_head_t
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
等待队列中的元素为wait_queue_t
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
struct task_struct * task;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
等待队列链表中的每个元素代表一个睡眠进程,该进程等待某一事件的发生;进程的描述符存放在task指针中。task_list字段链接等待相同事件的所有进程。
有时候,唤醒等待队列中所有睡眠进程是不合适的,例如等待的是某打印设备。因此将睡眠进程分为互斥进程和非互斥进程。func字段用来表示等待队列中睡眠进程应该用什么方式唤醒,非互斥进程内核提供唤醒函数default_wake_function。
2.4.2 等待队列的操作
add_wait_queue(q, wait) /* 将非互斥等待队列元素wait插入等待队列q */
add_wait_queue_exclusive(q, wait) /* 将互斥等待队列元素wait插入等待队列q */
内核使用prepare_to_wait、prepare_to_wait_exclusive、finish_wait来使当前进程在一个等待队列中睡眠。典型应用如下:
DEFINE_WAIT(wait)
prepare_to_wait_exclusive(&wq, &wait, TASK_INTERRUPTIBLE);
...
if (!condition)
schedule();
finish_wait(&wq, &wait);
函数prepare_to_wait_exclusive用传递的第三个参数设置进程的状态,然后把等待队列元素的互斥标志flag设置为1(互斥),最后插入等待队列。
进程一旦被唤醒就执行finish_wait函数,它把进程的状态再次设置为TASK_RUNNING,并从等待队列中删除。
内核通过下面的任何一个宏唤醒等待队列中的进程并把它们的状态设置为TASK_RUNNING:
wake_up(x)
wake_up_nr(x, nr)
wake_up_all(x)
wake_up_interruptible(x)
wake_up_interruptible_nr(x, nr)
wake_up_interruptible_all(x)
wake_up_locked(x)
wake_up_interruptible_sync(x)
每个宏的名字可以明白其功能:
- 所有宏都考虑到处于TASK_INTERRUPTIBLE状态的睡眠进程;如果宏的名字不含interruptible,那么处于TASK_UNINTERRUPTIBLE状态的睡眠进程也被考虑到。
- 所有宏都唤醒具有请求状态的所有非互斥进程
- 名字中含有nr的宏唤醒具有给定数量的具有请求状态的互斥进程;这个数量是宏的参数nr。名字中含有all的宏唤醒具有请求状态的所有互斥进程。最后名字中不含nr或all的宏只唤醒具有请求状态的一个互斥进程。
- 名字中不含有sync的宏检查被唤醒进程的优先级是否高于系统正在运行进程的优先级,并在必要时调用schedule()。造成的结果是高优先级进程的执行稍有延迟。
2.5 进程资源限制
每个进程都有一组相关的资源限制,限制指定了进程能使用的系统资源数量。这些限制避免进程过分使用系统资源。对当前进程的资源限制存放在current->signal-rlim[]中。