日志标签 ‘fork’

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函数了。这是下篇文章要完成的工作。

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