《linux内核设计与实现-笔记2》

第 3 章 进程管理

这一章主要介绍进程在linux系统中的表示,从如何创建到如何被回收的整个过程。同时特别提到了线程在linux系统中的实现。

进程的定义:

进程就是处于执行期的程序,但进程不仅局限于可执行代码,还包括其他资源,比如打开的文件,挂起的信号,内核数据,处理器状态,多个线程,全局变量等。

线程的定义:
线程是进程中活动的对象,线程有单独的程序计数器,进程栈,一组进程寄存器。值得注意的是,unix系统中,一个进程包括一个线程,但linux系统中,线程和进程其实不区分的(内核角度)。

虚拟机制:虚拟处理器和虚拟内存。线程之间共享虚拟内存,但每个有各自的虚拟处理器。

记住,进程是执行期的程序 + 相关资源

进程创建通常是通过fork()系统调用来实现,通过复制现有进程来创建全新进程,调用一次,返回两次,一次回到父进程,返回子进程PID,一次回到子进程,返回0。一般创建新的进程都是为了执行新的程序,接着调用exec()可以创建新的地址空间。linux中,fork()实际上是clone()系统调用实现的。

最终程序通过exit()退出执行,释放资源。父进程通过wait()系统调用获得子进程状态,这时,子进程才从僵死状态中,完全被释放掉。

进程描述符及任务结构

进程列表在内核中是一个任务队列的双向循环链表。结点的数据类型是task_struct,称为进程描述符。包含进程所有信息,在32位机器上,大约有1.7kb。

任务队列

linux 通过slab分配器分配task_struct结构。这样的目的是对象复用和缓存着色。避免动态分配和释放带来的资源消耗

进程创建时会产生两个栈,一个用户空间的进程栈,另一个是属于内核空间的内核栈。进程调用系统调用陷入内核态就会发生栈的切换。实际上只是栈的地址的变动。这里需要注意的是,从用户空间陷入内核态时,内核栈已经是清空的,因此,这时仅仅是将内核栈的栈顶指针给现在的栈地址寄存器就行。

进程内核栈是一个thread_union的结构如下:

union thread_union {
	struct thread_info thread_info;
	unsigned long stack[THEREAD_SIZE/sizeof(long)];
};

struct thread_info {
	struct task_struct   *task; // 注意
	struct exec_domain   *exec_domain;
	__u32                flg;
	__u32                status;
	__u32                cpu;
	int                  preempt_count;
	mm_segment_t         addr_limit;
	struct restart_block restart_block;
	void                 *sysenter_return;
	int                  uaccess_err;

};

struct task_struct {
	void *stack; // 注意

};

从代码中可以看出task_struct 和 thread_info 相互保持对方的指针。再看一个进程内核栈的布局图,我们就知道,可以通过计算直接得到thread_info的指针。

进程内核栈

因此,内核栈和进程描述符之间就有了一个座桥梁,可以相互引用。

PID标识每个进程,类型为pid_t,实际上是一个int类型,默认值最大32768.可以修改。这个值越小,转一圈就越快。

进程状态

系统中的进程必处于五种状态之一,如图:

进程状态图

可中断,进程处于睡眠(阻塞),等待某些条件达成,但可以被信号提前唤醒

不可中断,进程处于睡眠(组设),等待某些条件达成,不会被信号提前唤醒

进程的状态可以设置,但是我们知道这里会需要设置内存屏障,强制其他处理器重新排序。避免不一致状态:)

进程家族树

linux系统中的进程有明显的继承关系。原因是将进程的创建分为fork()/exec()两个阶段。所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程,该进程读取初始化脚本并执行其他的相关程序,最终完成系统的启动过程。

进程描述符中,有parent指针,指向父进程,有children list,表示子进程链表。还有任务队列中的next、prev指针,指向任务队列的前一个和下一个进程。

进程创建

刚才说到,进程的创建和特别(unix,linux)。许多其他的系统是通过spawn进程机制,创建新的地址空间,读入可执行文件,最后开始执行。而linux采用fork()和exec()。fork拷贝当前进程,创建一个子进程,区别仅仅在于PID、PPID和一些统计量(挂起的信号等)。exec负责读取可执行文件,开始运行。

传统的fork,直接复制所有资源给新进程。简单但效率低下,因为大多数新进程都打算立即执行exec,那之前的拷贝就毫无意义。linux的fork使用的是写时拷贝(copy on write)页实现。延迟拷贝的过程,内核不复制整个进程地址空间,而父子进程共享一个进程地址空间。只有需要写入时,数据才会被赋值。实际大部分新进程都是立即执行exec,不需要复制原有的进程数据。能够快速创建进程。

fork的实际开销是,复制父进程的页表以及给子进程创建唯一的进程描述符。

线程在linux中的实现

我们一般来说,线程是轻量级的进程,进程中包含若干线程。同一程序的线程共享内存地址空间,包括打开的文件和其他资源。linux实现线程非常特别,从内核的角度,没有线程的概念。linux把所有的线程当成进程来实现。线程仅仅被视为和其他进程共享某些资源(地址空间)的进程。每个线程有自己的task_struct,所以在内核看来就是一个普通的进程。想想,实际这种方法很高明。

线程的创建时调用clone(),但是传入了CLONE_VM参数,表示共享地址空间。

进程终止

进程终止需要释放它占用的资源,包括task_struct。进程的析构是自身引起的,通过调用exit()系统调用,释放资源,包括打开的文件描述符,定时器等。最后调用exit_notify给父进程发送信号,给子进程重新找养父,养父为同一进程组的其他进程或者init进程。进程的状态为EXIT_ZOMBIE。进程处于这个僵死状态,它现在占用的内存就是内核栈、thread_info结构和task_struct结构。唯一的目的就是向它的父进程提供信息。父进程检索到信息后,通知内核那是无关信息,进程剩余内存被释放掉。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值