进程描述符
1、简介
具体进程的信息保存在进程描述符 task_struct 中,在32位系统中有大约1.7KB,结构定义在 <linux/sched.h> 中,包含的进程信息有:进程状态(volatile long state),优先级(int prio),调度实体(struct sched_entity se),进程地址空间(struct mm_struct *mm, *active_mm),进程退出状态,进程号(pid_t pid),线程组号(pid_t tgid)①,时间记账,打开的文件描述符,信号等等。
不同进程描述符使用双向链表(list_head)组织。Linux通过slab分配 task_struct 结构。
2、thread_info
x86下,Linux在内核栈尾端(内核栈最低内存地址)分配 thread_info 结构②,用于更快的查找进程描述符并节省内核栈空间:
struct thread_info {
struct task_struct * task;
...
};
其中的task属性指向该进程的进程描述符,Linux使用 current_thread_info() 函数获得当前运行进程的 thread_info 对象,使用 current 宏(调用 current_thread_info() 函数)获得当前运行进程的进程描述符。
x86下 current_thread_info() 核心代码为
return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
其中sp为内核栈指针,一般Linux为内核栈分配1页的空间(早期有可能是2页),THREAD_SIZE为内核栈大小,栈最低地址必定是栈大小的整数倍,‘与’操作即可返回栈最低地址 。在其他的一些有较多寄存器的体系结构中,thread_info 可以保存在寄存器里,这样访问更快。
3、进程状态
进程包含若干种状态:
- TASK_RUNNING:进程正在运行或者已经就绪(在调度队列中等待CPU调度)
- TASK_INTERRUPTIBLE:进程睡眠,等待某些条件达成(收到信号、中断)
- TASK_UNINTERUPTIBLE:同上,唯一区别是进程不会被信号唤醒,比较罕见,说明该进程正在进行重要的操作
- __TASK_TRACED:调试进程时的状态
- __TASK_STOPPED:进程停止运行,如收到SIGSTOP信号
在执行 ps 命令后,R代表正在运行或就绪的进程,S代表可中断睡眠的进程,D代表不可中断睡眠的进程
内核通过 set_task_state(task, state) 改变进程的状态
4、进程树
Linux的进程存在明显的继承关系,每个进程有一个父进程,有0个或多个子进程
可以通过 task_struct 结构中的 parent 属性获得父进程的进程描述符,init进程的进程描述符是 init_task,遍历子进程方法如下:
struct task_struct *task;
struct list_head *list;
3
list_for_each(list, ¤t->children) {
task = list_entry(list, struct task_struct, sibling);
/* task 指向某个子进程 */
}
进程关系图
进程创建
Linux使用 fork() 函数通过拷贝当前进程创建子进程,调用 exec() 函数读入可执行文件并载入到地址空间运行。由于存在写时拷贝,fork 的实际开销只有复制父进程页表和创建子进程的进程描述符。
fork系列函数(vfork,clone等)都会调用do_fork(),do_fork()定义在 kernel/fork.c 中,do_fork会调用 copy_process() 函数,此函数真正进行复制操作,具体流程如下:
- 调用 dup_task_struct() 返回一个新的进程描述符,创建新的内核栈、thread_info结构
- 检测进程数量是否超过最大进程数
- 初始化子进程和兄弟进程链表,时间记账等等
- 复制进程信息,如文件描述符、进程地址空间等等
- 调用alloc_pid()分配有效的pid
- 做扫尾工作,返回子进程进程描述符指针
线程创建
线程创建与进程创建类似,只是传给clone的参数不一样,共享地址空间、文件系统资源、文件描述符、信号处理程序的几个进程即被称作线程
进程终结
当一个进程终结时(正常调用exit()退出或者出现异常),内核会调用 do_exit() 函数释放进程资源并通知父进程,具体流程如下:
- 设置进程标志(tsk->flags)为 PF_EXITING:exit_signals(tsk);
- 设置退出码: tsk->exit_code = code
- 释放进程的地址空间: exit_mm(tsk);
- 释放进程占用的文件、文件系统资源
- 为子进程找到新的父亲(同一线程组的其他线程或者init进程)
- 发送信号通知父进程
- 调用 schedule() 切换到其他进程
踩到的坑
1、可以通过调用 vfork 系统调用来产生一个 TASK_UNINTERUPTIBLE 的进程。调用该系统调用后,父进程会等待子进程运行结束或调用了exec后再运行,此时父进程即处于不可中断睡眠状态。但我们仍然可以通过给父进程发送信号来终止父进程。原因是linux 2.6.25引入了新状态TASK_KILLABLE③。
参考资料