进程
定义
-
何为进程?
就是处于执行器的程序。但不仅仅局限于一段可执行程序代码,通常还包括其他资源,如打开的文件、挂起的信号、内核内部数据、处理器状态、一个或多个具有内存映射的内存地址空间及一个或多个执行线程,还包括用来存放全局变量的数据段等。(即 程序不是进程,进程是处于执行期间的程序及相关资源的总称) -
何为线程?
执行线程就是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。
内核调度的对象是线程。对于Linux而言,线程和进程并不区分,可以说线程只不过是一种特殊的进程。
进程提供两种虚拟机制
- 虚拟处理器
实际上是许多进程分享一个处理器,但是虚拟处理器给进程一个假象,以为自己独处处理器。 - 虚拟内存
虚拟内存让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源。
注意在同一个进程中的线程之间可以共享虚拟内存,但是每个都拥有各自的虚拟处理器。
fork(), exec(), exit(), wait4()
- fork()
在Linux系统中,通常调用fork()创建进程,该系统调用通过复制一个现有进程来创建一个新的进程。
调用fork()的进程为父进程,新产生的进程为子进程。
fork() 系统调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程。 - exec()
调用exec()这组函数可以创建新的地址空间,并把新的程序载入其中。 - exit()
程序通过exit()系统调用退出执行,会终结进程并将其占用的资源释放掉。 - wait4()
父进程可通过wait4()系统调用查询子进程是否终结。
进程又叫任务(task)
进程描述符及任务结构
进程列表被存放在叫做任务队列的双向循环链表中。
链表中每一项是类型为 task_struct
的进程描述符结构,其包含一个具体进程的所有信息:它打开的文件、进程的地址空间、挂起的信号、进程状态等信息。
分配进程描述符
Linux通过 slab 分配器分配 task_struct
结构,只需在栈底(向下增长)或栈顶(向上增长)创建一个新的结构 struct thread_info
。
为什么?
因为这样能达到 对象复用 和 缓存着色:通过预先分配和重复使用task_struct
结构,可以避免动态分配和释放所带来的资源消耗。
struct thread_info
定义如下:
struct thread_info {
struct task_struct *task; // 存放的是指向该任务实际task_struct的指针
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;
};
进程描述符的存放
内核通过一个唯一的 PID 进程标识符(在进程描述符中)来标识每个进程。
PID的最大值默认设置为32868,这个最大值实际上就是系统中允许同时存在的进程的最大数目。
访问任务通常需要获得指向其task_struct的指针。因此,通过current宏查找到当前正在运行进程的task_struct的速度非常重要。
进程状态
task_struct中的state域描述了进程的当前状态,必是下列五种状态标志之一。
- TASK_RUNNING(运行):进程可执行的。
- TASK_INTERRUPTIBLE(可中断):进程正在睡眠(被阻塞)
- TASK_UNINTERRUPTIBLE(不可中断):处于等待队伍中,等待资源有效时唤醒(比如等待键盘输入、socket连接、信号等等),但不可以被中断唤醒。
- __TASK_TRACED:被其他进程跟踪的进程。
- __TASK_STOPPED(停止):进程停止执行。
设置当前进程状态
set_task_state(task, state); // 将任务task的状态设置为state
//等价于
task->state = state;
进程上下文
可执行程序代码从可执行文件载入到进程的地址空间执行。
一般程序在用户空间执行 —> 系统调用、中断或异常 —>陷入到内核空间。此时,称内核 “代表进程执行” 并处于进程上下文中。
这个时候用户空间的进程要传递很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。
进程家族树
进程之间会存在着一个明显的继承关系。
所有进程都是PID为1的init进程的后代;每个进程必有一个父进程(包含一个指向父亲,叫做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); // task为现在指向当前的某个子进程
}
所有进程之间的关系:
struct task_struct *task;
for(task = current; task != &init_task; task = task->parent)
// task现在指向init
这种继承体系可以从系统的任何一个进程出发查找到任意指定的其他进程。
大多数时,由于任务队列是双向循环链表,所以只需简单的方式就可遍历系统所有进程。
- 对于给定进程,获取链表中下一个进程:
list_entry(task->tasks.next, struct task_struct, tasks);
- 对于给定进程,获取链表中前一个进程:
list_entry(task->tasks.prev, struct task_struct, tasks);
进程创建
不同于其他操作系统通过 产生 进程的机制来创建进程,Unix创建进程是通过两个单独的函数来执行:
- fork()
fork()通过拷贝当前进程创建一个子进程。 - exec()
exec() 函数负责读取可执行文件并将其载入地址空间开始运行。
写时拷贝
Linux的fork() 使用写时拷贝页实现。只有在需要写入的时候,数据才会被复制。实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。
fork()
fork() 通过系统调用 clone()
实现,然后由 clone() 去调用 do_fork(),该函数再调用 copy_process()函数,然后让进程开始运行。
clone()
->do_fork()
->copy_process()
过程如下:
- 调用dup_task_struct()为新进程创建与当前进程值相同的一个内核栈、thread_info结构和task_struct。
copy_process()
->dup_task_struct()
- 检查当前用户拥有的进程数目没有超过给它分配的资源限制。【检查资源限制】
- 子进程开始与父进程区分开来,将task_struct内许多成员清0或设为初始值。【区分子进程父进程】
- 子进程状态设为 TASK_UNINTERRUPTIBLE ,保证不会运行。【子进程不运行】
- 调用 copy_flags() 来更新task_struct的flags成员。表明进程还没有调用exec()函数。【没exec()】
copy_process()
->copy_flags()
- 调用alloc_pid()为新进程分配一个有效PID。【分配PID】
copy_process()
->alloc_pid()
- 根据传递给clone的参数,copy_process() 拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。【拷贝共享】
- 最后做一些扫尾工作返回指向子进程指针。【返回指针】
vfork()
vfork()与fork() 区别只在于不拷贝父进程的页表项。
线程在Linux中的实现
线程机制提供了在同一程序内共享内存地址空间运行的一组线程,它们还可以共享打开的文件和其他资源。
线程机制支持并发程序设计技术,在多处理器系统上能保证真正的并行处理。
在Linux中,线程仅被视为一个与其他进程共享某些资源(如地址空间)的进程。而对于其他操作系统实现的线程机制,他们会在内核中提供专门支持线程的机制,称为 “轻量级进程”,这就是Linux与其他系统在此处的区别。
创建线程
与创建进程类似,只是在调用clone() 时需要传递一些参数标志来指明需要共享的资源:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONES_SIGHAND, 0)
这与调用fork()类似,只是父子俩共享地址空间、文件系统、文件描述符和信号处理程序,这父子俩进程就是所谓的“线程”。
clone() 有如下参数:
内核线程
内核经常需要在后台执行一些操作,可通过内核线程完成。
内核线程与普通进程的区别:内核线程没有独立的地址空间,只在内核空间运行,不会切换到用户空间中。
内核线程只能由其他内核线程创建。通过从kthreadd内核进程中衍生出所有新的内核线程来自动处理。创建一个新内核线程方法如下:
struct task_struct *kthread_create(int (*threadfn) (void *data),
void *data,
const char namefmt[],
...)
这里新创建的进程处于不可运行状态,可调用 wake_up_process()
来唤醒它。若想让其运行起来可通过调用 kthread_run()
:
struct task_struct *kthread_run(int (*threadfn) (void *data),
void *data,
const char namefmt[],
...)
进程终结
进程终结发生在进程调用 exit()
系统调用时。该任务大部分靠 do_exit()
来完成。将进程相关联的所有资源都释放掉。进程不可运行并处于EXIT_ZOMBIE退出状态。它只占用内核栈、thread_info 和 task_struct 结构。
删除进程描述符
do_exit()
之后系统还保存着 task_struct 结构,因此进程终止的清理工作和删除进程描述符分开执行。
通过调用 relase_task()
释放进程描述符。
孤儿进程造成的进退维谷
如果父进程在子进程之前退出,必须保证子进程能找到新爸爸,否则这些子进程会在退出时永远处于僵死状态。
解决方法:
给子进程在当前进程组内找一个线程作为父亲,如果不行,直接让init做它们父进程。
do_exit()
->exit_notify()
->forget_orginal_parent()
->find_new_reaper()