日志标签 ‘内核分析’

Linux2.6进程调度分析(3)-与调度有关的函数分析

2011年4月8日

前面两篇文章从原理角度分析了进程的调度,本文将从具体的源码出发,分析与进程进程调度密切相关的几个函数。

1.时间片的分配:task_timeslice()

正如我们所知的那样,进程的时间片与进程的静态优先级有直接的关系。从代码中可以看到,根据进程静态优先级static_prio与NICE_TO_PRIO(0)的大小关系,进程时间片的分配可以分为两条路线。以下代码如无特别说明均位于linux/kernel/sched.c下。

static unsigned int task_timeslice(task_t *p)
{
	if (p->static_prio < NICE_TO_PRIO(0)) 		return SCALE_PRIO(DEF_TIMESLICE*4, p->static_prio);
	else
		return SCALE_PRIO(DEF_TIMESLICE, p->static_prio);
}

NICE_TO_PRIO以及PRIO_TO_NICE宏的作用将进行nice值和进程静态优先级之间的转换。nice也用来表示进程的静态优先级,只不过它与静态优先级的大小范围不同,因此可以将nice看作是static_prio的缩影。

#define MAX_USER_RT_PRIO        100
#define MAX_RT_PRIO             MAX_USER_RT_PRIO
#define NICE_TO_PRIO(nice)	(MAX_RT_PRIO + (nice) + 20)
#define PRIO_TO_NICE(prio)	((prio) - MAX_RT_PRIO - 20)

目前我们已经知道普通进程的静态优先级大小范围是100到139,因此从上面的一些列宏可以得知,nice的取值范围为-20到19。

因此,NICE_TO_PRIO(0)取值为120,也就是说进程时间片分配的两条路线是根据静态优先级120进行划分的。从SCALE_PRIO宏的定义我们可以看到,该宏的作用是取两个数值(具体应该是时间片)的最大者。

#define MAX_PRIO                (MAX_RT_PRIO + 40)
#define USER_PRIO(p)		((p)-MAX_RT_PRIO)
#define MAX_USER_PRIO		(USER_PRIO(MAX_PRIO))
#define DEF_TIMESLICE		(100 * HZ / 1000)
#define MIN_TIMESLICE           max(5 * HZ / 1000, 1)
# define HZ             1000  //位于linux/include/asm-i386/param.h
#define SCALE_PRIO(x, prio) \
max(x * (MAX_PRIO - prio) / (MAX_USER_PRIO/2), MIN_TIMESLICE)

从上面的宏定义可知,(MAX_USER_PRIO/2)为20。当进程静态优先级小于120时,x的值为DEF_TIMESLICE*4,具体即为400ms;否则x为100ms。因此对于SCALE_PRIO宏可以用下面的公式来表达:

静态优先级<120,基本时间片=max((140-静态优先级)*20, MIN_TIMESLICE)

静态优先级>=120,基本时间片=max((140-静态优先级)*5, MIN_TIMESLICE)

其中MIN_TIMESLICE为系统所规定的最小时间片大小。

2.对可运行队列的操作

在优先级数组结构prio_array中,数组queue用来表示系统中每种优先级进程所形成的可运行队列,而且过期进程和活动进程分别对应这样一个数组。

如果进程仍然处于活动进程队列中,即说明该进程的时间片未用完。当该进程的时间片用完时就需要离开活动进程队列并进入过期进程队列。可运行进程进入进程队列是通过enqueue_task函数完成的,而离开进程队列是通过dequeue_task函数完成的。

每个进程的task_struct结构中都有list_head类型的run_list字段,将进程从可运行队列中删除起始就是对双联表的操作,同时我们需要更新优先级数组结构中活动进程的数目nr_active。如果当前进程优先级所对应的可运行队列已空,那么还要清除优先级位图中该进程优先级所对应的那个位。

如果要进程某个可运行队列,所做的工作基本上跟删除相反。不过该函数首先通过sched_info_queued函数更新该进程最后进入可运行队列的时间戳,并且在最后更新该进程描述符中的array字段,该字段指向当前进程所在的优先级数组。

 static void dequeue_task(struct task_struct *p, prio_array_t *array)
  {
          array->nr_active--;
          list_del(&p->run_list);
          if (list_empty(array->queue + p->prio))
                  __clear_bit(p->prio, array->bitmap);
  }

  static void enqueue_task(struct task_struct *p, prio_array_t *array)
  {
          sched_info_queued(p);
          list_add_tail(&p->run_list, array->queue + p->prio);
          __set_bit(p->prio, array->bitmap);
          array->nr_active++;
         p->array = array;
  }

3.schedule_tick()

schedule_tick函数用来更新进程的时间片,它被调用时本地中断被禁止,该函数的具体操作如下。

1.首先通过相应的函数和宏获得当前处理器的编号、当前可运行队列和当前进程描述符就,再通过sched_clock函数获得最近一次定时器中断的时间戳。如果array没有指向本地活动进程队列,则设置TIF_NEED_RESCHED标志,以便在稍候强制进程重新调度。

void scheduler_tick(void)
{
	int cpu = smp_processor_id();
	runqueue_t *rq = this_rq();
	task_t *p = current;

	rq->timestamp_last_tick = sched_clock();

	if (p == rq->idle) {
		if (wake_priority_sleeper(rq))
			goto out;
		rebalance_tick(cpu, rq, SCHED_IDLE);
		return;
	}
	if (p->array != rq->active) {
		set_tsk_need_resched(p);
		goto out;
	}

2.由于实时进程和普通进程的调度方法不同,因此这两种进程对时间片的更新方式也有所不同,下面仅说明普通进程更新时间片的方式。如果当前进程是普通进程,则递减当前进程的时间片。
3.如果当前进程时间片用完,首先从当前活动进程集合中删除该进程,然后通过set_tsk_need_resched函数设置TIF_NEED_RESCHED标志。

接着通过effective_prio函数更新当前进程的动态优先级,在进程调度的基本原理中我们已经知道进程的动态优先级是以进程的静态优先级(static_prio)为基数,在通过bonus适当的对其惩罚或奖励。

static int effective_prio(task_t *p)
{
	int bonus, prio;

	if (rt_task(p))
		return p->prio;

	bonus = CURRENT_BONUS(p) - MAX_BONUS / 2;

	prio = p->static_prio - bonus;
	if (prio < MAX_RT_PRIO) 		prio = MAX_RT_PRIO; 	if (prio > MAX_PRIO-1)
		prio = MAX_PRIO-1;
	return prio;
}

通过effective_prio函数,我们可以总结出进程动态优先级的计算规则:

动态优先级=max(100 , min(静态优先级 – bonus + 5) , 139)

再通过task_timeslice函数对当前进程重新分配时间片,因为我现在所处的分析环境是进程的时间片已经用完。然后将first_time_slice的值设置为0,说明当前进程的时间片可以用完。

上述过程的代码描述如下:

	if (rt_task(p)) {
		/*
		 *更新实时进程的时间片
		 */
	}
	if (!--p->time_slice) {
		dequeue_task(p, rq->active);
		set_tsk_need_resched(p);
		p->prio = effective_prio(p);
		p->time_slice = task_timeslice(p);
		p->first_time_slice = 0;

4.运行队列结构中的expired_timestamp字段用来描述过期进程队列中最早进程被插入队列的时间,如果本地运行队列中该字段等于0,则说明当前过期进程集合为空。因此将当前的时钟节拍赋值给该字段。

由于当前进程的时间片已经用完,因此接下来应该判定是将当前进程插入活动进程集合还是过期进程集合。可能此时你会有疑惑:既然当前进程的时间片已经用完,就应该直接插入过期进程队列,为何还要进行判断插入那个进程集合?

正如基本原理中所说的,调度程序总偏向交互进程以提高系统的响应能力。因此当交互型进程使用完时间片后,调度程序总是重新填充时间片并把它留在活动进程集合中。但调度程序并不永远都偏向交互型程序,如果最早进入过期进程集合的进程已经等待了很长时间,或过期进程的静态优先级比交互进程的静态优先级高,此时调度程序就会将时间片用完的交互进程转移到过期进程集合中。EXPIRED_STARVING宏完成的工作就是判断上述两种情况,至少其一否和,该宏就产生值1。

如果说当前进程已经移入到过期进程集合中,还需根据当前进程的优先级更新运行队列结构中的best_expired_prio字段,该字段用于记录过期进程集合中最高的静态优先级。

如果当前进程是交互进程,而且不满足EXPIRED_STARVING宏,则直接将该交互进程继续插入活动进程集合中。

		if (!rq->expired_timestamp)
			rq->expired_timestamp = jiffies;
		if (!TASK_INTERACTIVE(p) || EXPIRED_STARVING(rq)) {
			enqueue_task(p, rq->expired);
			if (p->static_prio < rq->best_expired_prio)
				rq->best_expired_prio = p->static_prio;
		} else
			enqueue_task(p, rq->active);

5.如果当前进程并未使用完时间片,则检查当前进程的剩余时间片是否太长。如果当前进程时间片过长的话,就将该进程的时间片分成若干个更小的时间段,这样可以防止拥有较长时间片的进程长时间霸占CPU。并且调度程序会将这样的进程放入与该进程优先级所对应的活动进程队列的末尾,稍候再次对其集成调度。

	} else {
		if (TASK_INTERACTIVE(p) && !((task_timeslice(p) -
			p->time_slice) % TIMESLICE_GRANULARITY(p)) &&
			(p->time_slice >= TIMESLICE_GRANULARITY(p)) &&
			(p->array == rq->active)) {

			requeue_task(p, rq->active);
			set_tsk_need_resched(p);
		}
	}

至此,该函数分析完毕。

fork系统调用分析(3)–copy_process()

2010年12月12日

copy_process()分析

通过上面的分析我们得知do_fork()主要完成以下的工作:为子进程定义了一个进程描述符并申请pid;调用copy_process()复制子进程;再通过clone_flags标志做一些复制后的辅助工作。copy_process()函数主要用来创建子进程的描述符以及与子进程相关数据结构。这个函数内部实现较为复杂,在短时间内,对于内部详细代码原理和实现并不能全部理解。因此,接下来的分析侧重于copy_process()的执行流程。

1. 定义返回值变量和新的进程描述符。

        int retval;
        struct task_struct *p = NULL;

2. 对clone_flags所传递的标志组合进行合法性检查。当出现以下三种情况时,返回出错代号:

(1). CLONE_NEWNS和CLONE_FS同时被设置。

前者标志表示子进程需要自己的命名空间,而后者标志则代表子进程共享父进程的根目录和当前工作目录,两者不可兼容。
传统的Unix系统中,整个系统只有一个已经安装的文件系统树。每个进程从系统的根文件系统开始,通过合法的路径可以访问任何文件。在2.6版本中的内核中,每个进程都可以拥有属于自己的已安装文件系统树,也被称为命名空间。通常大多数进程都共享init进程所使用的已安装文件系统树,只有在clone_flags中设置了CLONE_NEWNS标志时,才会为此新进程开辟一个新的命名空间。

(2). CLONE_THREAD被设置,但CLONE_SIGHAND未被设置。

如果子进程和父进程属于同一个线程组(CLONE_THREAD被设置),那么子进程必须共享父进程的信号(CLONE_SIGHAND被设置)。

(3). CLONE_SIGHAND被设置,但CLONE_VM未被设置。

如果子进程共享父进程的信号,那么必须同时共享父进程的内存描述符和所有的页表(CLONE_VM被设置)。

3. 通过调用security_task_create()和后面的security_task_alloc()执行所有附加的安全性检查。

4. 通过dup_task_struct()为子进程分配一个内核栈、thread_info结构和task_struct结构。

p = dup_task_struct(current);

注意,这里将当前进程描述符指针作为参数传递到此函数中。该函数内部的具体过程如下:

首先,该函数分别定义了指向task_struct和thread_inof结构体的指针。

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
	struct task_struct *tsk;
	struct thread_info *ti;

接着,为正式的分配进程描述符做一些准备工作。主要是将一些必要的寄存器的值保存到父进程的thread_info结构中。这些值会在稍后被复制到子进程的thread_info结构中。

	prepare_to_copy(orig);

执行alloc_task_struct宏,该宏负责为子进程的进程描述符分配空间,将该片内存的首地址赋值给tsk;随后检查这片内存是否分配正确。

	tsk = alloc_task_struct();
	if (!tsk)
		return NULL;

执行alloc_thread_info宏,为子进程获取一块空闲的内存区,用来存放子进程的内核栈和thread_info结构,并将此会内存区的首地址赋值给ti变量;随后检查是否分配正确。

	ti = alloc_thread_info(tsk);
	if (!ti) {
		free_task_struct(tsk);
		return NULL;
	}

上面已经说明过orig是指向当前进程描述符的指针。因此,先将当前进程的thread_info结构中的内容复制到ti变量;再将当前进程task_struct结构中的内容复制到tsk变量;让子进程描述符中的thread_info字段指向ti变量;最后让子进程thread_info结构中的task字段指向tsk变量。

	*ti = *orig->thread_info;
	*tsk = *orig;
	tsk->thread_info = ti;
	ti->task = tsk;

将子进程描述符的使用计数器设置为2,表示该进程描述符正在被使用并且处于活动状态。

	atomic_set(&tsk->usage,2);

最后返回指向刚刚创建的子进程描述符内存区的指针。

        return tsk;
}

通过上述代码可以看到,当这个函数成功操作之后,子进程和父进程的描述符中的内容是完全相同的。在稍后的代码中,我们将会看到子进程逐步与父进程区分开来。

5. 更新当前用户的user_struct结构。当前进程的用户如果没有root权限,并且所拥有的进程数大于所规定的进程数时,就返回错误代码。

接着对该user_struct结构的引用计数加1;对该用户所拥有的进程总数量加1。

        atomic_inc(&p->user->__count);
           atomic_inc(&p->user->processes);

6. 检测系统中进程的总数量是否超过了max_threads所规定的进程最大数。

         if (nr_threads >= max_threads)
                 goto bad_fork_cleanup_count;

7. 将从do_fork()传递来的的clone_flags和pid分别赋值给子进程描述符中的对应字段。

         copy_flags(clone_flags, p);
             p->pid = pid;

8. 逐步初始化子进程描述符中字段,使得子进程和父进程逐渐区别出来。这部分工作包含初始化双联表、互斥锁和描述进程属性的字段等。它在copy_process函数中占据了相当长的一段的代码,不过考虑到task_struct结构本身的复杂性,也就不足为奇了。

9. 根据clone_flags的具体取值,通过诸如copy_semundo()和copy_files()等这样的函数来为子进程拷贝或共享父进程的某些数据结构。

10. 通过copy_threads()函数更新子进程的内核栈和寄存器中的值。在之前的dup_task_struct()中只是为子进程创建一个内核栈,至此才是真正的赋予它有意义的值。

当父进程发出clone系统调用时,内核会将那个时候CPU中寄存器的值保存在父进程的内核栈中。这里就是使用父进程内核栈中的值来更新子进程寄存器中的值。特别的,内核将子进程eax寄存器中的值强制赋值为0,这也就是为什么使用fork()时子进程返回值是0。而在do_fork函数中则返回的是子进程的pid,这一点在上述内容中我们已经有所分析。另外,子进程的对应的thread_info结构中的esp字段会被初始化为子进程内核栈的基址。

11. 调用sched_fork函数,使得子进程的进程状态为TASK_RUNNING。并禁止内核抢占。并且,为了不对其他进程的调度产生影响,此时子进程共享父进程的时间片。

12. 根据clone_flags的值继续更新子进程的某些属性。

13. 将 nr_threads加一,表明新进程已经被加入到进程集合中。将total_forks加一,以记录被创建进程数量。

        nr_threads++;
           total_forks++;

14. 如果上述过程中某一步出现了错误,则通过goto语句跳到相应的错误代码处;如果成功执行完毕,则返回子进程的描述符p。

至此,copy_proces()的大致执行过程分析完毕。

do_fork()执行完毕后,虽然子进程处于可运行状态,但是它并没有立刻运行。至于子进程合适执行这完全取决于调度程序,也就是schedule(),本文并不涉及涉及此函数的分析。

fork系统调用分析(2)-do_fork()

2010年12月9日

do_fork()分析

从上文可得知, fork、vfork和clone三个系统调用所对应的系统调用服务例程均调用了do_fork()。只不过在调用时所传递的参数有所不同,而参数的不同正好导致了子进程与父进程之间对资源的共享程度不同。因此,分析do_fork()成为我们的首要任务。

在进入do_fork函数进行分析之前,很有必要了解一下它的参数。

clone_flags:该标志位的4个字节分为两部分。最低的一个字节为子进程结束时发送给父进程的信号代码,通常为SIGCHLD;剩余的三个字节则是各种clone标志的组合(本文所涉及的标志含义详见下表),也就是若干个标志之间的或运算。通过clone标志可以有选择的对父进程的资源进行复制;本文所涉及到的clone标志详见下表。

statck_start:子进程用户态堆栈的地址;

regs:指向pt_regs结构体的指针。当系统发生系统调用,即用户进程从用户态切换到内核态时,该结构体保存通用寄存器中的值,并被存放于内核态的堆栈中;

stack_size:未被使用,通常被赋值为0;

parent_tidptr:父进程在用户态下pid的地址,该参数在CLONE_PARENT_SETTID标志被设定时有意义;

child_tidptr:子进程在用户太下pid的地址,该参数在CLONE_CHILD_SETTID标志被设定时有意义;

do_fork函数的主要作用就是复制原来的进程成为另一个新的进程,它完成了整个进程创建中的大部分工作。

1. 在一开始,该函数定义了一个task_struct类型的指针p,用来接收即将为新进程(子进程)所分配的进程描述符。紧接着使用alloc_pidmap函数为这个新进程分配一个pid。由于系统内的pid是循环使用的,所以采用位图方式来管理。简单的说,就是用每一位(bit)来标示该位所对应的pid是否被使用。分配完毕后,判断pid是否分配成功。

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 pid = alloc_pidmap();

	if (pid < 0)
		return -EAGAIN;

2. 接下来检查当前进程(父进程)的ptrace字段。ptrace是用来标示一个进程是否被另外一个进程所跟踪。所谓跟踪,最常见的例子就是处于调试状态下的进程被debugger进程所跟踪。父进程的ptrace字段非0时说明debugger程序正在跟踪父进程,那么接下来通过fork_traceflag函数来检测子进程是否也要被跟踪。如果trace为1,那么就将跟踪标志CLONE_PTRACE加入标志变量clone_flags中。

通常上述的跟踪情况是很少发生的,因此在判断父进程的ptrace字段时使用了unlikely修饰符。使用该修饰符的判断语句执行结果与普通判断语句相同,只不过在执行效率上有所不同。正如该单词的含义所表示的那样,current->ptrace很少为非0。因此,编译器尽量不会把if内的语句与当前语句之前的代码编译在一起,以增加cache的命中率。与此相反,likely修饰符则表示所修饰的代码很可能发生。

	if (unlikely(current->ptrace)) {
		trace = fork_traceflag (clone_flags);
		if (trace)
			clone_flags |= CLONE_PTRACE;
	}

3. 接下来的这条语句要做的是整个创建过程中最核心的工作:通过copy_process()创建子进程的描述符,并创建子进程执行时所需的其他数据结构,最终则会返回这个创建好的进程描述符。该函数中的参数意义与do_fork函数相同,此函数的详细执行过程在本文的下一节有详细说明。

	p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);

4. 如果copy_process函数执行成功,那么将继续下面的代码。
首先定义了一个完成量vfork,如果clone_flags包含CLONE_VFORK标志,那么将进程描述符中的vfork_done字段指向这个完成量,之后再对vfork完成量进行初始化。

完成量的作用是,直到任务A发出信号通知任务B发生了某个特定事件时,任务B才会开始执行;否则任务B一直等待。我们知道,如果使用vfork系统调用来创建子进程,那么必然是子进程先执行。究其原因就是此处vfork完成量所起到的作用:当子进程调用exec函数或退出时就向父进程发出信号。此时,父进程才会被唤醒;否则一直等待。此处的代码只是对完成量进行初始化,具体的阻塞语句则在后面的代码中有所体现。

	if (!IS_ERR(p)) {
		struct completion vfork;

		if (clone_flags & CLONE_VFORK) {
			p->vfork_done = &vfork;
			init_completion(&vfork);
		}

5. 如果子进程被跟踪或者设置了CLONE_STOPPED标志,那么通过sigaddset函数为子进程增加挂起信号。signal对应一个unsigned long类型的变量,该变量的每个位分别对应一种信号。具体的操作是,将SIGSTOP信号所对应的那一位置1。

		if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {
			sigaddset(&p->pending.signal, SIGSTOP);
			set_tsk_thread_flag(p, TIF_SIGPENDING);
		}

6. 如果子进程并未设置CLONE_STOPPED标志,那么通过wake_up_new_task函数使得父子进程之一优先运行;否则,将子进程的状态设置为TASK_STOPPED。

		if (!(clone_flags & CLONE_STOPPED))
			wake_up_new_task(p, clone_flags);
		else
			p->state = TASK_STOPPED;

7. 如果父进程被跟踪,则将子进程的pid赋值给父进程的进程描述符的pstrace_message字段。再通过ptrace_notify函数使得当前进程定制,并向父进程的父进程发送SIGCHLD信号。

		if (unlikely (trace)) {
			current->ptrace_message = pid;
			ptrace_notify ((trace << 8) | SIGTRAP);
		}

8. 如果CLONE_VFORK标志被设置,则通过wait操作将父进程阻塞,直至子进程调用exec函数或者退出。

if (clone_flags & CLONE_VFORK) {
			wait_for_completion(&vfork);
			if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE))
				ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
		}

9. 如果copy_process()在执行的时候发生错误,则先释放已分配的pid;再根据PTR_ERR()的返回值得到错误代码,保存于pid中。

} else {
		free_pidmap(pid);
		pid = PTR_ERR(p);
	}

10. 返回pid。这也就是为什么使用fork系统调用时父进程会返回子进程pid的原因。至于为什么子进程会返回0则在copy_process()中有所体现。

以上便是do_fork函数的大致执行过程。至于子进程的进程描述符如何创建,就得分析copy_process函数了。这是下篇文章要完成的工作。

系统调用的执行过程

2010年12月8日

当用户态的进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。在X86体系中,可以通过两种不同的方式进入系统调用:执行int $0x80汇编命令和执行sysenter汇编命令。后者是Intel在PentiumII中引入的指令,内核从2.6版本开始支持这条命令。本文将集中讨论以int $0x80方式进入系统调用的过程。

通过int $0x80方式调用系统调用实际上是用户进程产生一个中断向量号为0x80的软中断。当用户态进程发出int $0x80指令时,CPU将从用户态切换到内核态并开始执行system_call()。这个函数是通过汇编命令来实现的,它是0x80号软中断对应的中断处理程序。对于所有系统调用来说,它们都必须先进入system_call(),也就是所谓的系统调用处理程序。再通过系统调用号跳转到具体的系统调用服务例程处。

在该函数执行之前,CPU控制单元已经将eflags、cs、eip、ss和esp寄存器的值自动保存到该进程对应的内核栈中。随之,在system_call内部首先将存储在eax寄存器中的系统调用号压入栈中。接着执行SAVE_ALL宏。该宏在栈中保存接下来的系统调用可能要用到的所有CPU寄存器。

 
/linux/arch/i386/kernel/entry.S
 241ENTRY(system_call)
 242        pushl %eax                      # save orig_eax
 243        SAVE_ALL

通过GET_THREAD_INFO宏获得当前进程的thread_inof结构的地址;再检测当前进程是否被其他进程所跟踪,也就是thread_inof结构中flag字段的_TIF_SYSCALL_TRACE或_TIF_SYSCALL_AUDIT被置1。如果发生被跟踪的情况则转向相应的处理命令处。

 
 244        GET_THREAD_INFO(%ebp)
 245        # system call tracing in operation
 246        testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
 247        jnz syscall_trace_entry

接着,对用户态进程传递过来的系统调用号的合法性进行检查。如果不合法则跳入到syscall_badsys标记的命令处。

 
 248        cmpl $(nr_syscalls), %eax
 249        jae syscall_badsys

如果系统调用好合法,则跳入相应系统调用号所对应的服务例程当中,也就是在sys_call_table表中找到了相应的函数入口点。由于sys_call_table表的表项占4字节,因此获得服务例程指针的具体方法是将由eax保存的系统调用号乘以4再与sys_call_table表的基址相加。

当系统调用服务例程结束时,从eax寄存器中获得当前进程的的返回值,并把这个返回值存放在曾保存用户态eax寄存器值的那个栈单元的位置上。这样,用户态进程就可以在eax寄存器中找到系统调用的返回码。

 250syscall_call:
 251        call *sys_call_table(,%eax,4)
 252        movl %eax,EAX(%esp)             # store the return value

至此,用户进程进入系统调用的过程大致分析完毕。

fork系统调用分析(1)-准备工作

2010年12月7日

接下来的几篇文章将会分析fork系统调用在内核中的实现,以作为本学期linux操作系统课程总的分析报告。本文所采用的源码均为v2.6.11。和最新内核相比,do_fork函数的变化较大,而核心函数copy_process则几乎保持不变。

操作系统需要一种机制用于创建新进程,fork()就是Linux或Unix提供给程序员用于创建进程的方法。fork函数的相关信息如下:

通常,我们程序中直接使用的fork函数是将fork系统调用封装之后而产生的。通过上面的表格,我们知道fork函数用于创建新的进程,所创建的进程称为当前进程的子进程,而当前进程也随之称为子进程的父进程。通常可以通过父子进程不同的返回值来区分父子进程,并使其执行不同功能的代码。

那么,用户态下的fork函数是如何调用fork系统调用的?内核中是如何创建子进程的?父子进程的返回值是如何产生的?这是本文所重点讨论的。

进程描述符

进程是操作系统中一个重要的基本概念。通常我们认为进程是程序的一次执行过程。为了描述和控制进程的运行,操作系统为每个进程定义了一个数据结构,即进程控制块(Process Control Block,PCB)。我们通常所说的进程实体包含程序段,数据段和PCB三部分,PCB在进程实体中占据重要的地位。所谓的创建进程,实质上就是创建PCB的过程;而撤销进程,实质上也就是对PCB的撤销。

上述内容是我们在操作系统原理课上所学习到的。在Linux内核中,PCB对应着一个具体的结构体——task_struct,也就是所谓的进程描述符(process descriptor)。该数据结构中包含了与一个进程相关的所有信息,比如包含众多描述进程属性的字段,以及指向其他与进程相关的结构体的指针。因此,进程描述符内部是比较复杂的。我们可以通过图1大致的了解进程描述符的结构。

图1(图片来自ULK)

可以看到,进程描述符中有指向mm_struct结构体的指针mm,这个结构体是对该进程用户空间的描述;也有指向fs_struct结构体的指针fs,这个结构体是对进程当前所在目录的描述;也有指向files_struct结构体的指针files,这个结构体是对该进程已打开的所有文件进行描述;另外还有一个小型的进程描述符(low-level information)——thread_info。在这个结构体中,也有指向该进程描述符的指针task。因此,这两个结构体是相互关联的。关于thread_info结构的详细说明可以参考本系列文章的后续分析。

与存储在磁盘上的程序相比,我们认为是进程是动态的。这是因为一个进程在其“一生”中可能处于不同的状态。因此,在进程描述符中,使用state字段对该进程当前所处的状态进行描述。在内核中,使用一组宏来描述进程可能所处的状态。这些宏之间是互斥的,也就是说进程一次最多只能使用一个宏。因为,进程在某一刻只可能处于一种状态。下面对本文中所涉及到的进程状态进行简单的描述:

可运行状态(TASK_RUNNING)
如果进程正在CPU上执行或者正在等待被调度程序所调度,那么它的状态就处于可运行状态。

暂停状态(TASK_STOPPED)
进程的执行被暂定,也就是我们常说的阻塞状态和等待状态。当进程接收到SIGSTOP、SIGSTP等信号时,就进入该状态。

跟踪状态(TASK_TRACED)
当一个进程被另一个进程跟踪监控时,这个进程就处于该状态。最常见的场景就是我们调试一个程序,被调试的程序就处于此状态。

fork系统调用

在用户态下,使用fork()创建一个进程对我们来说已经不再陌生。除了这个函数,一个新进程的诞生还可以分别通过vfork()和clone()。fork、vfork和clone三个API函数均由C库提供,它们分别在C库中封装了与其同名的系统调用fork(),vfork()和clone()。API所封装的系统调用对编程者是隐藏的,编程者只需知道如何使用这些API即可。

上述三个系统调用所对应的系统调用号在linux/include/asm-i386/unistd.h中定义如下:

   #define __NR_restart_syscall      0
   #define __NR_exit                 1
   #define __NR_fork                 2
   …… ……
   #define __NR_clone              120
   …… ……
   #define __NR_vfork              190
   …… ……

传统的创建一个新进程的方式是子进程拷贝父进程所有资源,这无疑使得进程的创建效率低,因为子进程需要拷贝父进程的整个地址空间。更糟糕的是,如果子进程创建后又立马去执行exec族函数,那么刚刚才从父进程那里拷贝的地址空间又要被清除以便装入新的进程映像。

为了解决这个问题,内核中提供了上述三种不同的系统调用。

1. 内核采用写时复制技术对传统的fork函数进行了下面的优化。即子进程创建后,父子以只读的方式共享父进程的资源(并不包括父进程的页表项)。当子进程需要修改进程地址空间的某一页时,才为子进程复制该页。采用这样的技术可以避免对父进程中某些数据不必要的复制。

2. 使用vfork函数创建的子进程会完全共享父进程的地址空间,甚至是父进程的页表项。父子进程任意一方对任何数据的修改使得另一方都可以感知到。为了使得双方不受这种影响,vfork函数创建了子进程后,父进程便被阻塞直至子进程调用了exec()或exit()。由于现在fork函数引入了写时复制技术,在不考虑复制父进程页表项的情况下,vfork函数几乎不会被使用。

3. clone函数创建子进程时灵活度比较大,因为它可以通过传递不同的参数来选择性的复制父进程的资源。具体参数可参见表1

就像一开始所分析的那样,用户程序并不直接使用系统调用,而是通过C库中的API。而系统调用在内核中也并不是直接实现的,而是通过调用各自对应的服务例程。系统调用fork、vfork和clone在内核中对应的服务例程分别为sys_fork(),sys_vfork()和sys_clone()。因此,想要了解fork等系统调用的详细执行过程,就必须查看它们所对应的内核函数(也就是服务例程)是如何实现的。上述三个系统调用对应的服务例程分别定义在linux/arch/i386/kernel/process.c 中,具体如下:

asmlinkage int sys_fork(struct pt_regs regs)
{
	return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
}

asmlinkage int sys_clone(struct pt_regs regs)
{
	unsigned long clone_flags;
	unsigned long newsp;
	int __user *parent_tidptr, *child_tidptr;

	clone_flags = regs.ebx;
	newsp = regs.ecx;
	parent_tidptr = (int __user *)regs.edx;
	child_tidptr = (int __user *)regs.edi;
	if (!newsp)
		newsp = regs.esp;
	return do_fork(clone_flags, newsp, ®s, 0, parent_tidptr, child_tidptr);
}

asmlinkage int sys_vfork(struct pt_regs regs)
{
	return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
}

可以看到do_fork()均被上述三个服务例程调用。而在do_fork()内部又调用了copy_process(),因此我们可以通过图2来理解上述的调用关系。

 

                                                                     图2
从上面的分析中,我们已经明确了do_fork()和copy_process()是本文的主要分析对象。但是在这之前,我们有必要先分析一下用户进程进入系统调用的过程。详见下文。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key