1.进程、轻量级进程和线程
- 引入进程的目的是担当恰当的分配系统资源的实体
- linux使用轻量级进程对多线程应用程序提供更好的支持
POSIX兼容的pthread库使用Linux轻量级进程有3个例子,它们是 LinuxThreads 、 Native Posix Thread Library(NPTL) 和IBM的下一代Posix线程包 NGPT(Next Generation Posix Threading Package)
2.进程描述符
- 为了管理进程,内核必须知道进程优先级、阻塞状态、分配的地址空间、允许访问的文件等等(task_struct描述)
2.1进程状态 --Task_struct.state
- TASK_RUNNING
- TASK_INTERRUPTIBLE
- TASK_UNINTERRUPTIBLE
- TASK_TRACED --正在被Debugger追踪
-
EXIT_ZOMBIE --僵死状态,也可放在Task_struct.exit_state
-
EXIT_DEAD --僵死撤销状态
2.2 标识一个进程
- 一般,能被独立调度的每个可执行上下文都必须拥有它自己的进程描述符(Task_struct,使用32位进程进程描述符地址标识进程)
- 另一方面,类UNIX允许使用process ID标识进程,在Task_struct.pid 字段,可往/proc/sys/kernel/pid_max写入一个小于32767的数减少PID的上限
- pidmap_array位图:标识分配/闲置的PID号(32768bit=4096bytes*8)
- 一个线程组中的所有线程使用该线程组的领头线程相同的PID,就是该组中第一个轻量级进程的PID,放在Task_struct.tgid(getpid()返回该值)
2.3 进程描述符处理
- 对每个进程来说,linux都把两个不同的数据结构紧凑的存放在一个单独为进程分配的存储区域内:一个内核态的进程堆栈,另一个是紧挨着进程描述符的小数据结构thread_info,叫线程描述符(2个页框),内核让着8k空间占据连续的两个叶匡),也可在编译时配置让跨一个y几个页框。
- 内核态进程访问处于内核数据段的栈(栈很少),对thread_info结构来说够了,但当使用一个页框存放内核态堆栈和thread_info结构时,内核要采用一些额外的栈以防止中断和异常的深度嵌套而引起的溢出。
- current_thread_info()获取thread_info结构的指针
movl $0xffffe000,%ecx /* 4K堆栈用0xfffff000 */ andl %esp,%ecx movl %ecx,p
- current宏获取CPU上在运行的描述符指针,等价于current_thread_info()->task
movl $0xffffe000,%ecx /* 4K堆栈用0xfffff000 */ andl %esp,%ecx movl (%ecx),p
2.4 双向链表
- linux内核定义了list_head数据结构,next和prev分别表示通用双向链表向前和向后的指针元素,不过,值得注意的是,这两个指针指向的是另一个list_head字段的地址,而不是list_head结构体的整个数据结构的地址。
- linux 2.6内核支持另一种双向链表,主要用于散列表,表头放在hlist_head中(指向第一个元素的指针,链表为空则为NULL)。每个数据元素都是hlist_node类型,next指向下一个元素,pprev指向前一个元素的next字段,第一个与最后一个元素的pprev的next为NULL
2.5 进程链表
- 把所有进程的描述符链接起来。每个task_struct结构都包含一个list_head类型的tasks字段,这个类型的prev和next字段分别指向前后的task_struct元素。
- 进程链表的头是init_task描述符,即0进程或swapper进程描述符,init_task.prev指向最后插入的进程描述符的task字段
- 宏SET_LINKS和REMOVE_LINKS用于插入和删除一个进程描述符
- 宏for_each_process扫描整个宏,list_entry_each(p,t,m)返回类型为t,t中含有list_head字段,list_head字段包含了名字m和p的地址
#define for_each_process(p) \ for((p=&init_task;(p=list_entry((p)->tasks.next, \ struct task_struct,tasks) \ ) != &init_task;))
2.6 TASK_RUNNING状态的进程链表
- 当内核寻找一个新进程在CPU上运行时,必须只考虑可运行进程(即处在TASK_RUNNING状态的进程)
- 提高调度程序运行速度的诀窍是建立多个可运行进程链表,每种进程优先权对应一个不同的链表。每个task_struct描述符包含一个list_head类型的字段run_list。如果进程的优先权=k,run_list把该进程链入优先权为k的可运行进程的链表中。(通过复制数据结构来改善性能)
- 内核必须为每个运行队列保存大量的数据,不过运行队列的主要数据还是组成运行队列的进程描述符表。所有这些链表都由一个单独的prio_array_t数据结构来实现
struct prio_array_t { int nr_active; //链表中进程描述符的数量 unsigned long [5] bitmap;//优先权位图:当且仅当某个优先权的进程链表不为空时设置相应的标志位 struct list_head[140] queue;//140个优先权队列的头结点 };
- enqueue_task(p,array)函数把进程描述符插入某个运行队列的链表
list_add_tail(&p->run_list,&array->queue[p->prio]); __set_bit(p->prio,array->bitmap);//prio存放进程动态优先权 array->nr_active++;//array指向当前运行队列的prio_array_t数据结构 p->array = array;
- dequeue_task(p,array)函数从运行队列中删除一个进程描述符
2.7 进程间的关系
- 进程描述符中表示进程亲属关系的字段的描述
- real_parent:指向创建了P的进程描述符,不存在则指向1(init)的描述符
- parent:指向P的当前父进程,当另一个进程发出监控P的ptrace()系统调用请求时该值与real_parent不同
- children:链表的头部,链表中的所有元素都市P创建的子进程
- sibling:指向兄弟进程链表中的下一个元素或前一个元素的指针,这些兄弟链表都是P创建的子进程
- pidhash表及链表
-
在几种情况下,内核必须能从进程的PID导出对应进程描述符指针。例如P1向P2发信号kill(p2id,SIGHUP),内核要从这个PID导出其对应的进程描述符,然后要从P2的进程描述符中取出记录挂起信号的数据结构指针。顺序扫描进程链表是低效的,为了加速引入了4个散列表
Hash表的类型 字段名 说明 PIDTYPE_PID pid 进程的PID PIDTYPE_TGID tgid 线程组领头的PID PIDTYPE_PGID pgrp 进程组领头的PID PIDTYPE_SID session 回话领头的PID -
宏pid_hashfn把PID转化为表索引
#define pid_hashfn(x) hash_long((unsigned long) x,pidhash_shift) unsigned long hash_long(unsigned long val,unsigned int bits){ unsigned long hash = val * 0x9e370001UL; return hash >>(32-bits); }
-
2.8 如何组织进程
- 等待队列表示一组睡眠的进程,当某一条件为真时由内核唤醒
- 每个队列都有一个等待队列头
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;//1表示互斥进程,由内核由选择的唤醒,0表非互斥进程,总是由内核在事件发生时唤醒 struct task_struct *task;//描述符地址 wait_queue_func_t func; struct list_head task_list;//该指针把元素链接到相同事件的进程链表中 }; typedef struct __wait_queue wait_queue_t;
- 每个元素代表一个睡眠的进程,
2.9 等待队列的操作
- 宏DECLEAR_WAIT_QUEUE_HEAD(name)定义一个新的等待队列头
- init_waitqueue_head(q,p)初始化动态分配的等待队列的头变量
q->flag=0; q->task=p; q->func=default_wake_function;
- DEFINE_WAIT声明一个wait_queue_t类型的新变量
- autoremove_wake_function()的地址与当前运行进程的描述符唤醒上述变量
- default_wake_function()唤醒睡眠进程后从等待队列链表中删除对应的元素
- init_waitqueue_func_entry()可以自定义唤醒函数
- add_wait_queue()把一个非互斥的j进程插入等待链表的第一个位置
- add_wait_queue_exclusive()把一个互斥进程插入到链表的最后一个位置
- remove_wait_queue()函数从等待队列链表中删除一个进程
- waitqueue_active()检查一个给定的等待队列是否为空
2.10 进程资源限制
- 对当前进程资源的限制放在了current->signal->rlim字段
struct rlimit { unsigned long rlim_cur; unsigned long rlim_max; };
3.进程切换
- 挂起正在CPU上运行的程序,并恢复以前挂起的某个进程的执行,这种就叫进程切换、任务切换或上下文切换
3.1 进程上下文
- 进程可以有自己的地址空间,但是必须共享CPU寄存器,在恢复执行之前,内核必须确保每个寄存器装入了挂起进程时的值。
- 进程恢复执行必须装入寄存器的一组数据叫硬件上下文(可执行上下文的一个子集),一部分放在TSS段,剩下放在内核态堆栈。
3.2 任务状态段
- 80x86包含一个特殊的任务状态段(Task State Segment)来存放硬件上下文(虽然linux不用硬件上下文),但强制为每个不同的CPU创建一个TSS.
- 80x86的一个CPU从用户态切换到内核态时就从TSS获取内核态堆栈地址
- 用户态进程试用in或out指令访问I/O端口时,CPU需要访问存在TSS中的IO许可权位图
- thread字段
- 每个进程描述符包含一个类型为thread_struct的thread字段,只要进程被切换出去,内核就把其硬件上下文保存在这个结构中。
3.3 执行进程切换
- 本质上说,进程切换由两步组成
- 切换页全局目录以安装一个新的地址空间
- 切换内核态堆栈和硬件上下文
- switch_to宏
#define switch_to(prev,next,last) \ do { \ last = __switch_to(prev,task_thread_info(prev), task_thread_info(next));\ } while (0)
- 第二步由这个宏执行,prev、next分别表示被替换进程和新进程描述符的地址在内存中的位置。
- …
3.4 保存加载FPU、MMX、XMM寄存器
- 为了与旧模式的兼容,浮点算术函数用ESCAPE指令来执行,如果有进程在使用ESCAPE指令,那么浮点寄存器的内容属于硬件上下文应该被保存。
- Pentium引入了MMX指令集来加速多媒体应用程序的执行,作用于FPU。
- Pentium III 引入了SSE扩展,为处理包含在XMM寄存器(8*128bit)的浮点值增加了功能
- 80x86并在TTS中自动保存FPU、MMX、XMM寄存器,不过它们包含某种硬件支持(cr0寄存器中的一个TS标志)
- 硬件执行硬件上下文切换时设置TS标志
- 每当TS标志被设置为ESCAPE、MMX、SSE或SSE2指令,控制单元就产生Device not available异常
- …(找不到可以印证的源码或看不懂)
4.创建进程
- 写时复制技术允许父子进程都相同的物理页
- 轻量级进程允许父子进程共享在内核中的很多数据结构如页表、打开文件表及信号处理
- vfork()创建的进程共享父进程的内存地址空间,为防止父进程重写子进程需要的数据,将阻塞父进程的执行
4.1 clone()、fork()、vfork()系统调用
- 轻量级进程由clone()创建
int clone(int (*fn)(void *), void *child_stack,int flags, void *arg, ... /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ ); extern long do_fork(unsigned long, unsigned long, struct pt_regs *, unsigned long, int __user *, int __user *);
- fn:新进程执行的函数,返回时子进程终止,返回的整数表示退出码
- arg:fn的参数
- flags:包括退出时发给父进程的信号代码与clone标志组用于编码
- ptid:父进程的用户天变量地址,在CLONE_PARENT_SETTID标志被设置才有意义
- ctid:新轻量级进程的用户态变量地址,在CLONE_CHILD_SETTID标志被设置才有意义
- 传统的fork()是由clone()实现的,这时flags参数指定为SIGCHLD信号及所有清0的clone标志,而child_stack是父进程当前的堆栈指针,就是说暂时共享一个用户态堆栈,改变栈时才得到用户态堆栈的一份拷贝。
- do_fork() --公司的(shell name -r 没找到源文件)
extern long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr);
- regs:指向通用寄存器值的指针(从用户态切换到内核态时被保存到内核态堆栈中的)
- do_fork()用辅助函数copy_process()来创建进程描述符及子进程所需要的其他数据结构
- 1.查找pidmap_array位图,分配PID
- 2.检查父进程的current->ptrace字段(是否被跟踪),…子进程不是内核线程就设置CLONE_PTRACE标志
- 3.调用copy_process()复制进程描述符,若所有必须资源可用则返回刚创建的task_struct地址
- 4.如果设置了CLONE_STOPPED或p->ptrace中设置了PT_PTRACED标志,那子进程被设置为TASK_STOPPED,并为子进程增加挂起的SIGSTOP信号,直到另一个进程把子进程恢复为TASK_RUNNING
- 5.如果没有设置CLONE_STOPPED标志,则调用wake_up_new_task():
- a.调整父进程和子进程的调度参数
- b.父子在同一CPU,且不能共享同一组页表(CLONE_VM=0),子插入父的运行队列,且在父前(父先运行写时复制将执行一系列不必要的页面复制)
- c.父子不在同一CPU,或共享同一组页表(CLONE_VM被设置),子插入父运行队列尾
- 6.若CLONE_STOPPED标志被设置,子进程为TASK_STOPPED状态
- 7.如果父进程被跟踪,子PID存入current->ptrace_message并调用ptrace_notify()使当前进程停止运行。并向当前进程的父发送SIGCHLD
- 8.如果设置CLONE_VFORK标志,则父插入等待队列,并挂起直到子进程释放自己的进程空间
- 9.结束并返回子进程的PID
- copy_process():创建进程描述符及子进程执行所需要的其他数据结构
- 1.检查clone_flags…
- 2.通过调用security_task_create()及稍后的security_task_alloc()执行所有附加安全检查
- 3.调用dup_task_struct()为子进程获取进程描述符
- a.若有需要,调用__unlazy_fpu()把FPU、MMX和SSE、SSE2寄存器的内容保存到thread_info结构中,稍后,dup_task_struct()将这些值复制到子进程的thread_info中
- b.alloc_task_struct()为新进程获取进程描述符
- c.执行alloc_thread_info()获取空闲内存,来放新进程的thread_info和内核栈,地址放在ti
- d.将current进程描述符复制到tsk指向的task_struct,然后tsk->thread_info置为ti
- e.current->thread_info复制到ti指向的结构,然后ti->task设置为tsk
- f.新进程描述符的使用计数器tsk->usage置为2,表示进程描述符正在被使用且处于活动状态
- g.返回新的进程描述符指针tsk
- 4…