本文有点长,建议分开食用。本文可以看做是介绍进程的一些特性,也可以看做是对进程描述符的各个重要字段的介绍。
1. 什么是进程
进程就是程序执行的一个实例。进程在创建时执行和父进程相同的代码,但是拥有自己的独立的数据拷贝(堆栈)。
所以,进程可以看做一个分配系统资源的实体。系统资源包括CPU时间、内存。
在linux源代码里,进程可以称之为task,也可以称之为thread。所以内核代码里看到这两个单词,都是在描述进程信息。
2. 轻量级进程与线程
线程thread就是进程的一个执行流,一个进程可以有多个执行流。所以说现代UNIX系统中,一个进程可以由多个线程组成。
假定我们正在执行一个聊天程序,这个聊天程序就是一个进程,那它至少要有两个执行流:一个负责响应用户的操作,比如说输入消息到聊天窗口,一个负责监听用户好友发来的消息。
那么显然这个聊天程序是一个多线程进程。那么这就造成一个问题:如果在内核中,把这个多线程的程序也当做是普通进程来执行,执行流的调度都发生在用户态,那么当用户操作聊天窗口的时候,监听消息的执行流就是阻塞状态,得不到执行。
所以,我们期待的是CPU以线程为单位进行调度。linux使用了轻量级进程,所谓轻量级进程就是相互之间可以共享某些资源的进程,轻量级进程和线程具有对应关系。一个多线程程序,内核用一个线程组来处理它。
面试中经常会问到进程和线程的区别,这里可以看出来一个重要的区别就是:进程是资源分配的基本单位,线程是调度的基本都单位。
3. 描述一个进程(x86_86)
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
struct thread_info *thread_info;
atomic_t usage;
unsigned long flags; /* per process flags, defined below */
unsigned long ptrace;
int lock_depth; /* Lock depth */
int prio, static_prio;
struct list_head run_list;
prio_array_t *array;
unsigned long sleep_avg;
long interactive_credit;
unsigned long long timestamp;
int activated;
unsigned long policy;
cpumask_t cpus_allowed;
unsigned int time_slice, first_time_slice;
struct list_head tasks;
struct list_head ptrace_children;
struct list_head ptrace_list;
struct mm_struct *mm, *active_mm;
/* task state */
struct linux_binfmt *binfmt;
int exit_code, exit_signal;
int pdeath_signal; /* The signal sent when the parent dies */
/* ??? */
unsigned long personality;
int did_exec:1;
pid_t pid;
pid_t __pgrp; /* Accessed via process_group() */
pid_t tty_old_pgrp;
pid_t session;
pid_t tgid;
/* boolean value for session group leader */
int leader;
/*
* pointers to (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->parent->pid)
*/
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 */
struct task_struct *group_leader; /* threadgroup leader */
/* PID/PID hash table linkage. */
struct pid_link pids[PIDTYPE_MAX];
wait_queue_head_t wait_chldexit; /* for wait4() */
struct completion *vfork_done; /* for vfork() */
int __user *set_child_tid; /* CLONE_CHILD_SETTID */
int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */
unsigned long rt_priority;
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_incr;
struct timer_list real_timer;
struct list_head posix_timers; /* POSIX.1b Interval Timers */
unsigned long utime, stime, cutime, cstime;
unsigned long nvcsw, nivcsw, cnvcsw, cnivcsw; /* context switch counts */
u64 start_time;
/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
/* process credentials */
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups;
gid_t groups[NGROUPS];
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
int keep_capabilities:1;
struct user_struct *user;
/* limits */
struct rlimit rlim[RLIM_NLIMITS];
unsigned short used_math;
char comm[16];
/* file system info */
int link_count, total_link_count;
struct tty_struct *tty; /* NULL if no tty */
/* ipc stuff */
struct sysv_sem sysvsem;
/* CPU-specific state of this task */
struct thread_struct thread;
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
/* namespace */
struct namespace *namespace;
/* signal handlers */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked, real_blocked;
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
void *security;
/* Thread group tracking */
u32 parent_exec_id;
u32 self_exec_id;
/* Protection of (de-)allocation: mm, files, fs, tty */
spinlock_t alloc_lock;
/* Protection of proc_dentry: nesting proc_lock, dcache_lock, write_lock_irq(&tasklist_lock); */
spinlock_t proc_lock;
/* context-switch lock */
spinlock_t switch_lock;
/* journalling filesystem info */
void *journal_info;
/* VM state */
struct reclaim_state *reclaim_state;
struct dentry *proc_dentry;
struct backing_dev_info *backing_dev_info;
struct io_context *io_context;
unsigned long ptrace_message;
siginfo_t *last_siginfo; /* For ptrace use. */
};
以上就是2.6内核源码中进程描述符task_struct的定义。描述符下面的字段很多,大概可以看到,一个进程有它的状态、有自己的parent,有对应的内存空间mm,信号signal_struct,文件files_struct等等。接下来就根据这些字段,介绍进程的一些比较重要的静态特性。
4. 进程的资源限制
对应的字段是struct rlimit rlim[RLIM_NLIMITS];大概限制了进程可以拥有的地址空间,CPU最长时间,可以打开的最大文件大小,等等。
5. 进程关系
可以是亲属关系,也有非亲属关系,对应的字段是:
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 */
struct task_struct *group_leader; /* threadgroup leader */
亲属关系,一个进程有它的父亲,儿子,和兄弟。所谓real_parent和parent,是当进程被另一个进程监控追踪时,它的parent指针会改变,而real_parent指针总是指向创建这个进程的进程。
前面提到了线程组,当这个进程是线程组的一员时,需要一个指针指向线程组的leader。
6. thread_info
进程描述符是放在动态内存中的。此外,内核每处理一个进程,都会分配一个2页的空间,单独存放2个内容,一个是进程描述符的thread_info字段,一个是该进程的内核态堆栈。thread_info在x86_64中长这样:
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
int preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
};
看到thread_info里也有个task指针,就能明白task和thread_info是一一对应的。
内核分配给进程的2页的空间,除去thread_info占用的之外,剩下的都是堆栈区。那么这片区域也可以这么表示:
union thread_union {
struct thread_info thread_info;
unsigned long stack[2048];
};
这片区域里,thread_info从低地址开始,stack从高地址开始向下增长,栈顶的地址存放在esp寄存器之中。栈里每写入一行数据,esp就会自减。
7. current
current是内核代码里的一个宏,用来指向当前正在处理的进程。它的原理是根据esp的值得到thread_info的首地址,因为他们都在一个连续的2页的空间内。假设是32位地址系统,thread_union就是2048*sizeof(unsigned long) = 2^13字节,所以屏蔽掉esp值的低13位就得到了thread_union的首地址,也就是thread_info的地址。thread_info的地址也就是thread_info.task的地址。
8. pid和tgid
每个进程描述符还有一个自己的pid。这个pid在创建进程的时候由系统分配,系统维护着一个pidmap_array位图,用这个来查找当前可用的pid。
pid_t pid;
pid_t __pgrp; /* Accessed via process_group() */
pid_t tty_old_pgrp;
pid_t session;
pid_t tgid;
另外,每个进程描述符里还有个tgid的字段,这是希望对线程组中的轻量级进程进行取pid操作时(getpid()),可以直接返回其所在线程组的领头线程的pid,而不是它真实的pid(当然了,如果这个线程就是领头线程,那它的pid=tgid)。
这保证了系统在操作一个进程的时候可以把它对应的整个线程组都操作到,比如kill()的时候。
系统中有一个全局变量叫pid_hash。
static struct list_head *pid_hash[PIDTYPE_MAX];
它包含4个hash表,分别保存了所有的pid,所有的线程组领头进程的pid,所有的进程组领头进程的pid,所有的会话领头进程的pid。
(*进程组:比如在shell里敲一个ls | sort | more命令,就为ls,sort和more这三个进程创建了一个进程组,这三个进程一起表示一种job的抽象)
9. 进程链表
struct list_head tasks;
进程描述符里这个字段,让所有进程都在一个双向链表上,这个链表的开端是个全局变量init_task,它指向swapper进程的描述符。所以内核里可以以init_task描述符为起始,遍历到当前存在的所有进程。
#define for_each_process(p) \
for (p = &init_task ; (p = next_task(p)) != &init_task ; )
10. 运行链表和优先级队列
int prio, static_prio;
struct list_head run_list;
prio_array_t *array;
内核在执行的时候,会发生进程调度,当前运行的进程让出cpu,换上来一个新进程。这个进程必须是可运行状态的,怎么快速找到一个可运行状态的进程呢?每个进程描述符都有一个run_list字段,把这个进程挂在一个可运行队列上。进程拥有静态优先级static_prio和动态优先级prio。其中动态优先级影响进程调度时调度策略的选择。它的取值范围是[0, 139]。
进程描述符下还有一个指针array指向一个prio_array_t结构,看prio_array_t这个结构体:
typedef struct prio_array {
int nr_active;
unsigned long bitmap[5];
struct list_head queue[140];
}prio_array_t;
array->queue[1]就表示一个优先级为1的队列,里面每个节点都是一个优先级为1的进程。
11. 进程状态
那么怎么表示一个进程是可运行状态还是不可运行呢?进程描述符里有一个state字段。它的取值包括TASK_RUNNING,TASK_INTERRUPTIBLE,TASK_UNINTERRUPTIBLE,TASK_STOPPED,TASK_TRACED,EXIT_ZOMBIE等。
其实run_list里挂的就是一些状态为TASK_RUNNING的进程。而在等待状态中的进程则是TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE的。
12. 等待队列
我们知道RUNNING状态的进程有自己的单独队列,那么其他状态的进程呢?
僵尸状态和暂停状态的进程,访问简单,直接通过pid找就行了,不需要单独建立队列。
等待状态(TASK_UNINTERRUPTIBLE或者TASK_INTERRUPTIBLE)的进程,是需要单独建立一个等待队列的。等待队列表示一组睡眠的进程,当某个条件发生时,就会唤醒其中的进程。
等待队列的头结点定义如下:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
等待队列的元素定义如下:
struct __wait_queue {
unsigned int flags;
struct task_struct * task;
wait_queue_func_t func;
struct list_head task_list;
};
一个等待队列的元素,除了指向进程描述符之外,还有func字段,指向唤醒时要执行的函数。flags字段表示这个进程是互斥进程(flags=1)还是非互斥进程(flags=0)。互斥进程相互之间竞争同一个互斥资源,所以在唤醒的时候一次只能唤醒一个,而非互斥进程没这个限制,唤醒时会把等待队列里的所有进程都唤醒。
把一个进程加入等待队列的方法有很多,这里只介绍一个把当前进程current加入等待队列q的函数sleep_on():
#define SLEEP_ON_VAR \
unsigned long flags; \
wait_queue_t wait; \
init_waitqueue_entry(&wait, current);
#define SLEEP_ON_HEAD \
spin_lock_irqsave(&q->lock,flags); \
__add_wait_queue(q, &wait); \
spin_unlock(&q->lock);
#define SLEEP_ON_TAIL \
spin_lock_irq(&q->lock); \
__remove_wait_queue(q, &wait); \
spin_unlock_irqrestore(&q->lock, flags);
void sleep_on(wait_queue_head_t *q)
{
SLEEP_ON_VAR
current->state = TASK_UNINTERRUPTIBLE;
SLEEP_ON_HEAD
schedule();
SLEEP_ON_TAIL
}
过程就是初始化一个指向当前进程的wait_queue_t,然后加入等待队列q,schedule(),当被唤醒时再把这个wait_queue_t从q中移出。
当某个条件为真的时候,就调用wake_up函数把队列q唤醒。过程是遍历q,调用wait_queue_t.func唤醒,直到队列空或者唤醒了一个互斥进程。
(在等待队列中,非互斥进程总是在队列前面,互斥进程总是在队列尾部。当然,一个等待队列里既有非互斥进程又有互斥进程的情况并不常见。)
13. 总结
本文介绍了什么是进程,线程和轻量级进程,并概括介绍了2.6内核代码中的进程描述符。
进程具有自己的状态,内存空间,打开的文件等一系列属性,进程具有父子兄弟,进程在执行时会有一个2页的内存空间用来存放自己的thread_info描述符和堆栈,栈顶的地址被写在esp寄存器中,内核可以通过读取esp寄存器中的值,倒推出thread_info的地址,进而得到当前执行的进程的描述符的地址(用current代表)。
除了通过current取到当前执行的进程外,系统要找到一个进程,第一可以通过pid,内核里定义了4个pid_hash。第二,所有进程都通过tasks字段挂在了一起,首进程(swapper)的描述符被写在全局变量init_task中,系统可以通过init_task.tasks.next遍历完所有的进程。
另外,所有可运行状态的进程都处在某个优先级的运行队列上,等待状态的进程也会存在于一个等待队列上。