Linux内核对比学习系列(2)——进程管理

前言

本篇文章主要对比一下linux0.12linux2.6在进程管理上的区别

进程描述符

进程描述符指的task_struct结构体,用于存储所有与任务(进程)执行相关的内容,接下来从以下两个方面对比两个版本task_struct的不同

  • 通过task_struct如何管理任务(进程)
  • task_struct如何存储

Linux 0.12

在fork过程的copy_process()的实现中,我们可以知道该版本对 task_struct 的管理十分简单

  1. 通过get_free_page()向主存申请一块物理页存放 task_struct
  2. 并将其置于task[NR_TASKS]数组中,该数组存储 task_struct 的地址

linux-0.12\kernel\fork.c

struct task_struct * task[NR_TASKS] //linux-0.12\kernel\sched.c

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
		long ebx,long ecx,long edx, long orig_eax, 
		long fs,long es,long ds,
		long eip,long cs,long eflags,long esp,long ss)
{
	struct task_struct *p;
	int i;
	struct file *f;

	p = (struct task_struct *) get_free_page();
	if (!p)
		return -EAGAIN;
	task[nr] = p;
	// --------------------------------省略无关代码----------------------------
	return last_pid;
}

Linux 2.6

该版本我们先根据《Linux内核设计与实现》对 task_struct 的管理一睹为快

内核把进程的列表存放在叫做任务队列的双向循环链表中,链表中的每一项都是类型为task_struct称为进程描述符的结构。该结构定义在<linux/sched.h>文件中。进程描述符包含一个具体进程的所有信息。

在2.6以前的内核中,各个进程的 task_struct存放在它们内核栈的尾端。这样做是为了让那些像 x86 那样寄存器较少的硬件体系结构只要通过栈指针就能计算出它的位置,而避免使用额外的寄存器专门记录。由于现在用 slab 分配器动态生成 task struct,所以只需在栈底(对于向下增长的栈来说)或栈顶(对于向上增长的栈来说)创建一个新的结构 struct thread info。每个任务的 thread info 结构在它的内核栈的尾端分配。结构中 task域中存放的是指向该任务实际 task_struct 的指针。

在内核中,访问任务通常需要获得指向其 task_struct 的指针。实际上,内核中大部分处理进程的代码都是直接通过 task struct进行的。因此,通过 current 宏查找到当前正在运行进程的进程描述符的速度就显得尤为重要。硬件体系结构不同,该宏的实现也不同,它必须针对专门的硬件体系结构做处理。有的硬件体系结构可以拿出一个专门寄存器来存放指向当前进程 task struct 的指针,用于加快访问速度。而有些像x86这样的体系结构(其寄存器并不富余),就只能在内核栈的尾端创建 thread_info 结构,通过计算偏移间接地查找 task struct结构。

可以得出,该版本的多个 task_struct 通过双向链表进行组织,通过 thead_info 间接指向 task_struct。之所以用链表而非数组,其好处就与链表与数组区别相关,在此不赘述。从描述中没有得出 task_struct 的具体存放方式,后续对比fork时进一步比较
在这里插入图片描述

进程创建

Unix 的进程创建很特别。许多其他的操作系统都提供了产生(spawn)进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix 采用了与众不同的实现方式,它把上述步骤分解到两个单独的函数中去执行∶fork()和exec(O9。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID(每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的 PID)和某些资源和统计量(例如,挂起的信号,它没有必要被继承)。exec(O)函数负责读取可执行文件并将其载入地址空间开始运行。把这两个函数组合起来使用的效果跟其他系统使用的单一函数的效果相似。

Linux 0.12

该版本的 fork() 与 exec() 过程已经分析过,可直接回顾
《Linux内核学习系列(3)——任务(进程)创建》
《Linux内核学习系列(7)——execve与需求加载》

fork()主要调用copy_process()完成新进程 task_struct 创建与拷贝,可以看到如下实现,是比较简单地

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
		long ebx,long ecx,long edx, long orig_eax, 
		long fs,long es,long ds,
		long eip,long cs,long eflags,long esp,long ss)
{
	struct task_struct *p;
	int i;
	struct file *f;

	p = (struct task_struct *) get_free_page();
	if (!p)
		return -EAGAIN;
	task[nr] = p;
	*p = *current;	/* NOTE! this doesn't copy the supervisor stack */
	p->state = TASK_UNINTERRUPTIBLE;
	p->pid = last_pid;
	p->counter = p->priority;
	p->signal = 0;
	p->alarm = 0;
	p->leader = 0;		/* process leadership doesn't inherit */
	p->utime = p->stime = 0;
	p->cutime = p->cstime = 0;
	p->start_time = jiffies;
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;
	p->tss.ss0 = 0x10;
	p->tss.eip = eip;
	p->tss.eflags = eflags;
	p->tss.eax = 0;
	p->tss.ecx = ecx;
	p->tss.edx = edx;
	p->tss.ebx = ebx;
	p->tss.esp = esp;
	p->tss.ebp = ebp;
	p->tss.esi = esi;
	p->tss.edi = edi;
	p->tss.es = es & 0xffff;
	p->tss.cs = cs & 0xffff;
	p->tss.ss = ss & 0xffff;
	p->tss.ds = ds & 0xffff;
	p->tss.fs = fs & 0xffff;
	p->tss.gs = gs & 0xffff;
	p->tss.ldt = _LDT(nr);
	p->tss.trace_bitmap = 0x80000000;
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0 ; frstor %0"::"m" (p->tss.i387));
	if (copy_mem(nr,p)) {
		task[nr] = NULL;
		free_page((long) p);
		return -EAGAIN;
	}
	for (i=0; i<NR_OPEN;i++)
		if (f=p->filp[i])
			f->f_count++;
	if (current->pwd)
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
	if (current->library)
		current->library->i_count++;
	set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->p_pptr = current;
	p->p_cptr = 0;
	p->p_ysptr = 0;
	p->p_osptr = current->p_cptr;
	if (p->p_osptr)
		p->p_osptr->p_ysptr = p;
	current->p_cptr = p;
	p->state = TASK_RUNNING;	/* do this last, just in case */
	return last_pid;
}

Linux 2.6

该版本在进程的基础上,还引入了线程的概念,设计clone()封装了fork(),能够通过改变clone()的参数调用不同模式的fork(),完成线程或者进程的创建。书中对此进行了总结

线程的创建和普通进程的创建类似,只不过在调用 clone()的时候需要传递一些参数标志来指明需要共享的资源∶
clone(CLONE VM| CLONE PS| CLONE_FILES | CLONE SIGHAND,0);
上面的代码产生的结果和调用 fork(差不多,只是父子俩共享地址空间、文件系统资源、文件描述符和信号处理程序。换个说法就是,新建的进程和它的父进程就是流行的所谓线程。
对比一下,一个普通的 fork的实现是∶
clone (SIGCHLD, 0);
而vfork的实现是∶
clone (CLONE_VFORK|CLONE_VM| sIGCHLD,0);
传递给 clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类。表3-1列举了这些clone()用到的参数标志以及它们的作用,这些是在<linux/sched.h>中定义的。

对于Linux内核而言,线程与进程本质上没有区别,只是线程之间可以共享内存及地址空间等内容而已。因此,只需要在fork的过程中,如果是线程,则对应资源指针指向同一处即可。

因此,不同于0.12版本fork的调用链 fork()->sys_fork()->copy_process,该版本的调用链为clone()->sys_clone()->do_fork()->copy_process()

kernel/fork.c

long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      struct pt_regs *regs,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
{
	struct task_struct *p;
	int trace = 0;
	long nr;

	// --------------------此处省略部分代码---------------------------
	p = copy_process(clone_flags, stack_start, regs, stack_size,
			 child_tidptr, NULL, trace);
	// --------------------此处省略部分代码---------------------------
	return nr;
}

跟踪copy_process(),代码很长,就不贴了,但实际上完成的事情就是根据clone_flags判断新进程的task_struct什么应该变什么不应该变

贴上书中对于fork的描述,个人认为总结得很好

Linux 通过 clone() 系统调用实现 fork() 。这个调用通过一系列的参数标志来指明父、子进程需要共享的资源。fork()、vfork() 和__clone() 库函数都根据各自需要的参数标志去调用 clone() ,然后由 clone() 去调用 do fork()。
do_fork完成了创建中的大部分工作,它的定义在 kernel/fork.c 文件中。该函数调用copy_process()函数,然后让进程开始运行。copy_process()函数完成的工作很有意思∶

1)调用 dup task_struct() 为新进程创建一个内核栈、thread_info 结构和 task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。

2)检查并确保新创建这个子进程后, 当前用户所拥有的进程数目没有超出给它分配的资源的限制。

3)子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0或设为初始值。那些不是继承而来的进程描述符成员,主要是统计信息。task_struct 中的大多数数据都依然未被修改。

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()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。

小结

可以看到,2.6版本的进程或线程创建,并没有对其展开说明。因为在理解了0.12的基础上,对这部分内容能够较好理解,个人就不打算进一步做笔记了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值