注:对linux系统来说,线程只是一种特殊的进程,linux不区分线程和进程;
进程:处于执行期的程序+包含的资源
一、进程描述符及任务结构
进程存放在任务队列(task list)的双向循环链表中;链表中每一项类型均为task_struct(进程描述符,也可表示为task_t),该结构定义在include/linux/sched.h文件中。
1. 分配进程描述符
- 通过slab分配器分配task_struct结构,可达到对象复用和缓存着色的目的;
- 现在用slab分配器动态生成task_struct,需要在栈低地址处放上新结构thread_info,使得汇编代码中计算其偏移比较容易:
struct thread_info在文件<arch/x86(或者其他架构)/include/asm/thread_info.h>中定义,X86体系结构中,thread_info内容:
struct thread_info {
struct task_struct *task; //指向进程描述符的指针
struct exec_domain *exec_domain; /* execution domain */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable, <0 => BUG */
mm_segment_t addr_limit;
struct restart_block restart_block;
void __user *sysenter_return;
#ifdef CONFIG_X86_32
unsigned long previous_esp; /* ESP of the previous stack incase of nested (IRQ) stacks*/
__u8 supervisor_stack[0];
struct task_struct *task; //指向进程描述符的指针
struct exec_domain *exec_domain; /* execution domain */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable, <0 => BUG */
mm_segment_t addr_limit;
struct restart_block restart_block;
void __user *sysenter_return;
#ifdef CONFIG_X86_32
unsigned long previous_esp; /* ESP of the previous stack incase of nested (IRQ) stacks*/
__u8 supervisor_stack[0];
#endif
int uaccess_err;
};
int uaccess_err;
};
2.进程描述符的存放
- 进程标识符(pid):一个数,表示为pid_t类型,实际上是int类型,pid默认最大值为32768,管理员修改/proc/sys/kernel/pid_max来提升pid最大值(系统允许的最大进程数目)
- 若内核栈+thread info结构的大小为8K,则:thread info为栈基址,可通过汇编代码计算thread info首址,对应函数:current_thread_info():
mov 0xffffe000 %eax
andl %esp , %eax ;eax保存了thread info首址
然后从thread info中提取进程描述符指针即可:
task_struct* p_process = current_thread_info()->task,对应的汇编代码:
mov 0xffffe000 %eax
andl %esp , %eax ;eax保存了thread info首址
mov %eax , p ;task在结构thread_info中偏移为0,所以thread_info首址即为进程描述符指针;
以上汇编代码为current宏,可得到进程描述符指针;
c语言实现:
return (struct thread_info *)
(current_stack_pointer & ~(THREAD_SIZE - 1));
其中THREAD_SIZE-1是内核栈+thread_info大小,一般为4KB/8KB
3.进程状态
- TASK_RUNNING(可运行状态):进程可执行;正在执行或者处于就绪状态,等待被运行;是进程在用户空间中唯一可能的状态。
- TASK_INTERRUPTIBLE(可中断状态):进程正在睡眠,某条件为真,内核将唤醒进程,将其状态设置为可运行;
- TASK_UNINTERRRUPTIBLE(不可终端):即时接收到信号,进程也不会被唤醒,通常在进程必须在等待时不受干扰或等待时间很快就会发生时出现,比较有用,如进程打开设备文件,相应的设备驱动程序开始探测相应硬件设备时会用到该状态,探测完成之前,设备驱动不可被中断;
- TASK_STOPPED:进程执行被在暂停;
- TASK_TRACED:跟踪状态,进程的执行被debugger程序暂停。
- TASK_ZOMBIE(僵死状态):进程已经结束,但其父进程没有调用wait4()系统调用,为了父进程可以获知其信息,子进程的进程描述符仍被保留,一旦父进程调用wait4(),进程描述符被释放;
进程状态转换图:
设置当前进程状态的方法:
p->state = state(p是进程描述符指针) <=> set_current_state(state) <=> set_task_state( current , state )(注:current宏获取当前进程描述符指针)
4.进程上下文
- 系统调用和异常处理程序是内核明确定义的接口,进程只有调用这些接口才能陷入内核执行,对内核的访问必须通过该接口,若某程序调用了系统调用,触发了某个异常,称内核“代表进程执行”并处于进程上下文中(因为调用结束后,还会返回用户进程继续处理)
5.进程家族
- 所有进程都是PID为1的init进程的后代,内核在系统启动最后阶段启动init进程;
- 系统中所有进程都有父进程(task_struct类型指针parent),每个进程可以有0个或多个子进程(list_head类型链表children),父进程相同的进程成为兄弟。
6.进程间联系(双向链表)
- 双向链表:字段next/prev分别指向后一个/前一个结构,每个数据结构成员list_head 指针,next/prev指针。
7.进程创建
- Unix进程创建方法:fork()+exec()—fork()通过拷贝当前进程创建一个子进程,子进程和父进程区别仅在于PID、PPID(父进程ID)和某些资源和统计量(如:挂起的信号等);exec()负责读取可执行文件,将其载入地址空间开始运行。
(1)写时拷贝(copy-on-write)
- Linux调用fork()时,内核并不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝,只有需要写入时,数据才被复制,在此之前,以只读的方式共享副本。将地址空间的拷贝推迟到写操作。
(2)fork()—通过clone系统调用实现
- do_fork()完成了进程创建中大部分工作,在kernel/fork.c文件中
- 调用dup_task_struct(current_proc)为新进程创建内核栈、thread_info和task_struct结构(此时父子进程描述符相同)
- 检查新创建子进程,当前用户拥有的进程数目没有超过分配给其的资源限制;
- 子进程描述符内的许多成员都被清0或者设置为初值;
- 子进程状态被设置为TASK_UNINTERRUPTIBLE,保证其不会投入运行;
- 调用copy_process(),更新task_struct的flags成员,表明进程是否是超级用户权限的PF_SUPERPRIV标识被清0,表明进程还没有调用的exec函数的PF_FORKNOEXEC标识被设置;
- 调用get_pid()为进程获取一个有效的PID
- 根据传递给clone()的参数标识,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
- 父进程和子进程平分剩余的时间片;
- copy_process()做扫尾工作,并返回指向子进程的指针。
- clone()系统调用返回到do_fork()函数,若copy_process()返回成功,新创建的子进程会被唤醒并投入运行;
8.进程终结(exit()函数,调用系统调用do_exit())
- 将task_struct中的标记设置为PF_EXITING;
- 调用del_timer_sync()删除任一内核定时器,根据返回结果,确保没有定时器在排队,也没有定时器处理程序在运行;
- 若BSD的进程开启记账功能,do_exit()调用acct_process输出记账信息;
- 调用exit_mm()函数放弃进程占用的mm_struct,若果没有其他进程在使用它们,则彻底释放该内存;
- 调用exit_sem()函数,若进程排队等候IPC信号,则离开队列;
- 调用exit_files,exit_fs,exit_namespace,exit_sighand分别递减文件描述符、文件系统数据、进程名字空间、信号处理函数的引用计数,若某个引用计数的值降为0,则代表没有进程使用相应资源,可以释放资源;
- 将存放在task_struct中的exit_code成员中的任务退出码设置为exit提供的退出代码,或者完成其他由内核机制规定的退出动作,退出代码供父进程检索;
- 调用exit_notify函数向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者init进程,并将子进程状态设置为EXIT_ZOMBIE;
- do_exit()调用schedule切换到新的进程;
此时进程占用的所有内存:内核栈、thread_info和task_struct结构,为父进程提供信息,父进程检索到信息后,可以将子进程所有剩余内存资源释放。
9.删除进程描述符(release_task)
- release_task:释放进程描述符
调用__exit_signal()函数,将进程从detach_pid()中删除;同时从任务列表中删除进程;
释放僵死进程所有剩余资源,进行最终统计和记录;
若进程是线程组最后一个进程,且通知僵死进程领头进程的父进程;
调用put_task_struct()释放进程内核栈和thread_info结构占的页,并释放task_struct占的slab告诉缓存。
注:
当一个进程被跟踪时,它的临时父亲设定为调试进程,若父进程退出,系统会为它和它所有的兄弟节点重新找一个父进程;以前内核中,遍历熊素有进程来找子进程,现在解决方法是在一个单独的被ptrace跟踪的子进程链表中搜索相关兄弟进程,减轻遍历带来的开销;
- wait()调用系统调用wait4()实现的:挂起调用它的进程,知道其中一个子进程退出,此时函数返回该进程的PID;