第三章 进程管理
一、基本的概念的术语解释
- 进程:处于执行期间的程序以及相关资源的总称,就是正在执行的程序代码的实时结果。
- 线程:是在进程中活动的对象,内核调度的对象就是线程。 注意线程可以共享虚拟内存,但是每个线程有各自的虚拟处理器
二、核心结构体
1、进程描述符
task_struct,包含的一个具体进程的所有信息:它打开的文件、进程的地址空间、挂起的信号、进程的状态等。
2、分配进程描述符
struct thread_info,Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色(cache coloring),并只需在栈顶创建一个新的结构struct thread_info,该结构体的task域中存放着该任务实际task_struct的指针。
struct thread_info {
struct task_struct *task; //指向该任务实际的描述符的指针
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
int preempt_count ;
mm_segment_t addr_limit;
struct restart_block restart_block ;
void sysenter_return;
int uaccess_err;
};
3、进程描述符的存放
进程通过PID来标识每个进程 ,得到描述符的操作一般由current宏来负责,在不同的硬件上会有不同的实现
X86的硬件体系,一般是采用计算偏移值的方式得到;(低效)
有的硬件则是通过拿出一个专门的寄存器来存放当前正在运行的描述符指针(高效,需要多余的寄存器)
三、进程的状态
1、 进程间的状态转换
三、进程上下文
1、概述
进程在执行中,可执行代码是非常重要的,这些代码需要载入到进程的地址空间所执行,当程序调用执行了系统调用或触发某个异常,则会陷入内核空间,我们称此为“内核在代表进程执行”并处于进程上下文。此时上下文的current宏是有效的(中断上下文就没效了,因为系统不代表进程执行了,是中断处理程序在执行),也就意味着可能有优先级更高的进程需要执行让调度器进行调整;如果没有这种情况的发生,内核退出后会恢复在用户空间进行执行
2、进程家族树
-
parent指针和children链表
每个描述符中都包含一个指向其父进程的描述符,称为parent指针,还包含一个为children的子进程链表 。
//对于当前进程,可以通过下面的代码获得其父进程的进程描述符:
struct task_struct *my_parent = current->parent;
//同样,也可以按以下方式依次访问子进程:
struct task_struct *task ;
struct list_head *list;
list_for_each ( list,¤t- >children) {
//指向某个子进程
task = list_entry(list, struct task_struct,sibling);
}
-
进程间的遍历获取
每个进程必有一个父进程,相应的,每个进程也可以拥有零个或多个子进程。
//所有进程都是PID为1的init进程的后代
struct task_struct *task ;
/* task现在指向init */
for (task = current; task != &init_task; task = task->parent)
//获取后一个进程及前一个进程的方法:
list_entry (task->tasks.next,struct task_struct,tasks)
list_entry(task->tasks.prev,struct task_struct, tasks)
//循环遍历整个任务队列,大量任务的时候不会这样做的
struct task_struct *task ;
for_each_process (task) {
/*它打印出每一个任务的名称和 PID*/
printk ( "s[d]ln" , task->comm,task->pid);
}
四、进程的创建
1、概述
通过两个函数fork和exec来实现进程的创建
- 通过fork函数,得到一个子进程,子进程和父进程区别在于:不同的PID、PPID(父进程的ID)和某些资源的统计量(挂起的信号等)
- exec函数将可执行文件加载到地址空间开始执行
2、fork函数的实现
Linux通过clone
函数系统调用fork
,调用通过一系列的参数指明父子进程需要共享的资源,大部分的fork
库函数clone
库函数都需要底层调用do_fork
函数,该函数完成了创建中的大部分工作,该函数调用copy_process
函数,让进程开始运行
3、写时拷贝
Linux的fork通过写时拷贝页,该技术可以推迟甚至免除拷贝数据的技术,内核此时并不复制整个进程的地址空间,而是让父进程子进程共享同一个拷贝 。
五、线程的创建
1、概述
线程的创建其实跟进程一样,只不过在调用clone函数传递的参数需要指明共享的资源(父子共享地址空间、文件系统资源、文件描述符和信号处理程序等)
//线程实现,第一个参数表明共享的资源
clone (CLONE_VM | CLONE_FS CLONE_FILES | CLONE_SIGHAND,0);
//普通fork实现
clone (SIGCHLD,0);
//vfork实现
clone (CLONE_VFORK | CLONE_VM | SIGCHLD,0);
2、clone函数的参数含义
六、内核线程
1、概述
内核线程就是指独立运行在内核的标准进程在后台执行一些操作(flush、ksofirqd),一直运行在内核不会切换到用户空间,内核线程和普通进程一样,可以被调度,可以被抢占。
2、创建、运行与终结
内核线程的创建还是基于clone的调用,新的进程通过这一个函数被创建后处于不可运行状态,如果不显式的去调用wake_up_process()明确地唤醒它,它不会主动运行。创建一个进程并让它运行起来,可以通过调用kthread_run():该函数就是将创建和唤醒简单的封装了一下
//创建
struct task_struct *kthread_create(int (*threadfn)(void *data),
void *data,
const char namefmt []....)
//运行
struct task_struct *kthread_run(int (*threadfn)(void *data),void *data,const char namefmt[],...)
{
struct task_struct *k;
k = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__);
if (!IS_ERR(k))
wake_up_process(k);
}
//终结,一直运行到调用do_exit()或内核其他线程调用stop函数
int kthread_stop(struct task struct *k)
七、进程终结
1、概述
当一个进程终结,内核必须释放他所占的资源并把去世的消息告知父进程,这个终结行为一般由自己引发(显式或隐式),一般都是通过do_exit()函数来完成 。
2、进程终结步骤
(1)将tast_strcut的标志成员设置为退出状态
(2)调用del_timer_sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也没有 定时器处理程序在运行
(3)查看是否开启了BSD的进程记账,然后调用相应方法进程输出记账信息
(4)调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们(也就是说,这个地址空间没有被共享),就彻底释放它们。
(5)接下来调用sem _exit()函数。如果进程排队等候IPC信号,它则离开队列。
(6)调用exit_files()和exit_fis(),以分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用计数的数值降为零,那么就代表没有进程在使用相应的资源,此时可以释放。
(7)接着把存放在task_struct的exit_code成员中的任务退出代码置为由exit()提供时退出代码,或者去完成任何其他由内核机制规定的退出动作。退出代码存放在这里供父进程随时检索。
(8)调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者为init进程,并把进程状态(存放在task_struct结构的exit_state中)设成EXIT_ZOMBIE。
(9)do_exit()调用schedule()切换到新的进程。因为处于EXIT_ZOMBIE 状态的进程不会再被调度,所以这是进程所执行的最 后一段代码。do_exit()永不返回。