【Linux内核设计与实现】进程管理

1.进程

进程是处于执行期的程序(目标码存放在,某种存储介质上)。

进程除了包含一段可执行程序代码,还包含其他资源,比如打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,以及用来存放全局变量的数据段等。

进程提供两种虚拟机制:虚拟处理器和虚拟内存

这两种虚拟给进程造成一个假象——只有自己独享处理器以及独享真个内存资源。

有趣的是,同一进程中的线程可以共享虚拟内存,但每个都拥有各自的虚拟处理器。

Linux系统中,系统调用fork()通过复制一个现有进程来创建一个全新的进程。fork()从内核返回两次:一次回到父进程,一次回到新产生的子进程。父进程返回的是子进程的id,子进程返回的是0。

最终,程序调用exit()系统调用退出执行,它会终结进程并将其占用的资源释放掉。父进程可以通过wait4()系统调用查询子进程是否终结。进程退出执行后被设置为僵死状态,直到其父进程调用wait()或waitpid()为止。

2.进程描述符及任务结构

内核把进程的列表存放在叫做任务队列(task list)的双向循环列表中。

列表中每一项都是类型为task_struct的结构,称为进程描述符,该结构定义在< linux/sched.h >文件中,它包含一个具体进程的所有信息

进程描述符包含的数据能够完整地描述一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态等。

这里写图片描述

Linux中可以用ps命令查看所有进程的信息:

ps -eo pid,tid,ppid,comm

下面针对进程描述符展开叙述。

2.1分配进程描述符

Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色(参见12章)的目的(通过预先分配和复用,可避免动态分配和释放所带来的损耗)。

在以前task_struct是存放在内核栈尾端的,但是现在由于task_struct由slab分配器动态生成,所以只需要在内核栈尾端分配一个thread_info结构,结构中task域中存放的是指向该任务实际task_struct(由slab分配器动态生成)的指针。

在x86上,struct thread_info在文件 < asm/thread_info.h > 中定义如下:

struct thread_info {
    struct task_struct  *task;      /* main task structure */
    __u32           flags;      /* low level flags */
    __u32           status;     /* thread synchronous flags */
    __u32           cpu;        /* current CPU */
};

2.2进程描述符的存放

内核通过一个唯一的进程标识值或PID来标识每个进程,PID是一个数,表示为pid_t隐含类型(实际上就是一个int类型)。

内核把每个进程的PID存放在它们各自的进程描述符中。

PID的上限可以通过修改/proc/sys/kernel/pid_max来提高。

在内核中,要访问任务,首先要获得指向其task_struct的指针。该指针通过current宏来访问,不同体系结构的宏实现不同。有的使用一个专门寄存器来存放指向task_struct的指针,而x86由于寄存器不富余,因此只能在内核栈的尾端创建thread_info结构,通过计算偏移间接地查找task_struct结构。

current_thread_info()->task;

2.3进程状态

进程描述符中的stat域描述了进程的当前状态。一共有5中状态标志:

1.TASK_RUNNING(运行):进程是可执行的,它或者正在执行,或者在运行队列中等待执行。

2.TASK_INTERRUPTIBLE(可中断):进程正在睡眠(被阻塞),等待某些条件的达成。

3.TASK_UNINTERRUPTIBLE(不可中断):除了就算接收到信号也不会被唤醒或准备投入运行外,该状态与可中断状态相同。

4.TASK_TRACED:被其他进程跟踪的过程。

5.TASK_STOPPED(停止):进程停止执行。

这里写图片描述

2.4设置当前进程状态

set_task_state(task, state); //将任务task的状态设置为state

// set_current_state(state)等价于set_task_state(current, state)
// 参见 <linux/sched.h>

2.5进程上下文

一般程序运行在用户空间,当它执行了系统调用或者触发了某个异常,就会陷入内核空间,此时,称内核“代表进程执行”并处于进程上下文中。

系统调用和异常处理程序是对内核明确定义的接口,进程只有通过这些接口才能陷入内核执行——对内核的所有访问都必须通过这些接口。

2.6进程家族树

Linux系统中进程之间存在明显的继承关系。所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。

系统中每个进程必有一个父进程(除init进程外)。进程间的关系存放在进程描述符中,每个task_struct都包含一个指向父进程task_struct、叫做parent的指针,还包含一个称为children的子进程链表。

init进程的进程描述符是作为init_task 静态分配的。

3.进程创建

Linux使用fork()和exec()两个函数去执行进程创建。

fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID,PPID和某些资源和统计量(例如,挂起的信号)。

exec()函数负责读取可执行文件并将其载入地址空间开始运行。

3.1写时拷贝

传统的fork()直接把所有资源复制给新创建的进程。这种实现效率地下,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。

Linux的fork()使用写时拷贝(copy-on-write)页实现。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在被需要写入时,数据才会被真正复制。这样子,在页根本不会被写入的情况下(比如fork()之后立即exec()),它们就无须复制了。

因此实际上fork()的开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。

3.2fork()

Linux通过clone()系统调用实现fork()。

clone()通过一系列的参数标志来指明父、子进程需要共享的资源。

fork()、vfork()、__clone()都根据各自需要的参数标志去调用clone(),它转而去调用do_fork()。do_fork完成了创建中的大部分工作,它的定义在kernel/fork.c文件中。该函数调用copy_process()函数,然后让进程开始运行。

copy_process()函数完成的工作:

1.调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。

2.检查并确保当前用户的进程数目没有超出资源限制。

3.子进程着手使自己和父进程区分开来。进程描述符内的不继承父进程的成员被清0或者设为初始值。

4.子进程状态被置为TASK_UNINTERRUPTIBLE,以保证它不会投入运行。

5.copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。

6.调用alloc_pid()为新进程分配一个有效的PID。

7.根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。

8.copy_process()做扫尾工作并返回一个指向子进程的指针。

再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行,内核有意选择让子进程先运行,因为一般子进程会马上调用exec(),这样可以避免写时拷贝的额外开销。

4.线程在Linux中的实现

在Linux中,对内核来说,并没有线程这个概念,它把所有的线程当做进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。

每个线程都有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,比如地址空间)。

对于linux来说,线程只是一种进程间共享资源的手段

比如有一个包含4个线程的进程,linux仅仅创建4个进程并分配4个普通的task_struct结构,并指定它们共享某些资源而已。

4.1创建线程

线程的创建和普通进程的创建类似,只不过在调用clone()时需要传递一些参数标志来指明需要共享的资源:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

上面代码产生的结果跟fork()类似,只是父子俩共享地址空间、文件系统资源、文件描述符和信号处理程序。

一个普通的fork()的实现是:

clone(SIGCHLD, 0);

4.2内核线程

内核线程与普通进程的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm指针被设置为NULL)。它们只运行在内核空间,从来不切换到用户空间。

使用以下命令可以查看内核线程:

ps -ef

从现有内核线程中创建一个新的内核线程的方法如下:

// 定义于<linux/kthread.h>中

struct task_struct *kthread_create(int (*threadfn)(void *date),
                    void *data,
                    const char namefmt[],
                    ...)

新创建的进程需要通过wake_up_process()来明确唤醒。

5.进程终结

当一个进程终结时,内核必须释放它所占有的资源并把这一不幸告知其父进程。

不管进程怎么终结,该任务大部分要靠do_exit()(定义于kernel
/exit.c)来完成,其工作如下:

1.将task_struct中的标识成员设置为PF_EXITING;

2.调用del_timer_sync()删除内核定时器, 确保没有定时器在排队和运行;

3.调用exit_mm()释放进程占用的mm_struct;

4.调用sem__exit(),使进程离开等待IPC信号的队列;

5.调用exit_files()和exit_fs(),递减文件描述符、文件系统数据的引用计数。

6.把task_struct的exit_code设置为进程的返回值,供父进程随时检索;

7.调用exit_notify()向父进程发送信号,并把自己的状态设为EXIT_ZOMBIE;

8.调用schedule()切换到新进程继续执行。

因为进程的状态为EXIT_ZOMBIE,所以不再被调度,do_exit()永不返回。

至此,该进程关联的资源被释放掉了,但是它本身占用的内存还没有释放,比如创建时分配的内核栈,thread_info结构和task_struct结构等。此时进程存在的唯一目的是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,该进程所持有的剩余内存被释放,归还给系统使用。

5.1删除进程描述符

父进程通过调用wait()一族的函数来检测子进程是否存在。它的标准动作是挂起调用它的进程,直到其中一个子进程退出,此时函数会返回子进程的PID。此外,调用该函数时提供的指针会包含子函数退出时的退出代码。

当最终需要释放进程描述符时,release_task()会被调用,完成以下工作:

1.调用__exit_signal(),该函数调用_unhash_process(),后者又调用detach_pid()从pidhash上删除该进程,同时也要从任务列表中删除该进程。

2._exit_signal()释放目前僵死进程所使用的剩余资源,并进行最终统计和记录。

3.如果这个进程是线程组的最后一个进程,并且领头进程已经死掉,那么release_task()就要通知僵死的领头进程的父进程。

4.release_task()调用put_task_struct()释放进程内核栈和thread_info结构所占的页,并释放task_struct所占的slab高速缓存。

至此,进程描述符和所有进程独享的资源就全部释放掉了。

5.2孤儿进程造成的进退维谷

父进程收到子进程发送的exit_notify()信号后,将该子进程的进程描述符和所有进程独享的资源全部删除。

因此,必须要确保每个子进程都有父进程,如果父进程在子进程结束之前就已经结束了呢?

如果子进程的父进程已经退出了,那么子进程在退出时,exit_notify()函数会先调用forget_original_parent(),然后再调用find_new_reaper()来寻找新的父进程。

find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程。(init进程是在linux启动时就一直存在的)。

init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。

6.小结

我们讨论了Linux如何存放和表示进程(task_struct和thread_info),如何创建进程(通过fork(),实际上最终是clone()),如何把新的执行映像装入到地址空间(通过exec()系统调用族),如何表示进程的层次关系,父进程又是如何收集其后代信息(通过wait()系统调用族),以及进程最终如何消亡(强制或自愿地调用exit())。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值