字符设备驱动再学习

2011年1月15日 由 edsionte 没有评论 »

本学期一直在学习linux下到设备驱动开发,字符设备驱动是设备驱动开发中最基本和重要的一部分。前几天的考试让我意识到对这部分的内容理解的还不是很清楚,因此,很有必要再次理解学习字符设备驱动。

本文以全局内存字符设备globalmem为例,说明字符设备驱动的结构以及编写方法。

1.字符设备的数据结构

在linux内核中使用struct cdev来表示一个字符设备,如下:

//在linux/include/linux/cdev.h中
  12struct cdev {
  13        struct kobject kobj;
  14        struct module *owner;
  15        const struct file_operations *ops;
  16        struct list_head list;
  17        dev_t dev;
  18        unsigned int count;
  19};

下面对该数据结构的字段作简单解释:

owner:该设备的驱动程序所属的内核模块,一般设置为THIS_MODULE;
ops:文件操作结构体指针,file_operations结构体中包含一系列对设备进行操作的函数接口;
dev:设备号。dev_t封装了unsigned int,该类型前12位为主设备号,后20位为次设备号;

cdev结构是内核对字符设备驱动的标准描述。在实际的设备驱动开发中,通常使用自定义的结构体来描述一个特定的字符设备。这个自定义的结构体中必然会包含cdev结构,另外还要包含一些描述这个具体设备某些特性到字段。比如:

struct globalmem_dev
{
	struct cdev cdev; /*cdev struct which the kernel has defined*/
	unsigned char mem[GLOBALMEM_SIZE]; /*globalmem memory*/
};

该结构体用来描述一个具有全局内存的字符设备。

2.分配和释放设备号

在linux中,对于每一个设备,必须有一个惟一的设备号与之相对应。通常会有多个设备共用一个主设备号,而具体每个设备都唯一拥有一个次设备号。总的来看,每个设备都唯一的拥有一个设备号。前面已经提到,内核使用dev_t类型来表示一个设备号,对于设备号有以下几个常用的宏:

//在linux/include/linux/kdev_t.h中
7#define MAJOR(dev)      ((unsigned int) ((dev) >> MINORBITS))
8#define MINOR(dev)      ((unsigned int) ((dev) & MINORMASK))
9#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

上述三个宏的功能分别为:通过设备号获取主设备号,通过设备号获取次设备号,通过主次设备好获取设备号。

在设备驱动程序中,一般会首先向系统申请设备号。linux中设备号的申请都是一段连续的设备号,这些连续的设备号都有共同的主设备号。设备号的申请有两种方法,若提前设定了主设备号则再接着申请若干个连续的次设备即可;若未指定主设备号则直接向系统动态申请未被占用到设备号。由此可以看出,如果使用第一种方法,则可能会出现设备号已被系统中的其他设备占用的情况。

上出两种申请设备号的方法分别对应以下两个申请函数:

  //在linux/fs/char_dev.c中
 196int register_chrdev_region(dev_t from, unsigned count, const char *name)
 232int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
 233                        const char *name)

上述两个函数都可以申请一段连续的设备号。前者适用已知起始设备号的情况(通过MADEV(major,0)可以获得主设备号为major的起始设备号);后者使用于动态申请设备号的情况。如果想申请一个设备号,则将函数中的参数count设为1即可。关于这两个函数的详细源码分析,可参考这里

3.Linux字符设备驱动的组成

实现一个基本的字符设备驱动需要完成以下几部分:字符设备驱动模块的加载卸载函数和实现file_operations结构中的成员函数。

3.1.file_operations结构体

file_operations结构体中包含许多函数指针,这些函数指针是字符设备驱动和内核的接口。,实现该结构中的这些函数也是整个字符设备驱动程序的核心工作。file_operations结构中的每个函数都对应一个具体的功能,也就是对设备的不同操作。不过,这些函数是在内核模块中实现的,最终会被加载到内核中和内核一起运行。因此,用户态下的程序是不能直接使用这些函数对相应设备进行操作的。

学过系统调用后,你就会知道,比如当应用程序通过系统调用read对设备文件进行读操作时,最终的功能落实者还是设备驱动中实现的globalmem_read函数。而将系统调用read和globalmem_read函数扯上关系的则是struct file_operations。具体的操作是:

static const struct file_operations globalmem_fops =
{
	.owner = THIS_MODULE,
	.read = globalmem_read,
	.write = globalmem_write,
	.open = globalmem_open,
	.release = globalmem_release,
};

3.2.实现加载和卸载函数

由于字符设备驱动程序是以内核模块的形式加载到内核的,因此该程序中必须有内核模块的加载和卸载函数。通常,字符设备驱动程序的加载函数完成的工作有设备号的申请、cdev的注册。具体的过程可参考下图:

globalmem_init流程图(点击看大图)

从上述的图中可以看到,在内核模块加载函数中主要完成了字符设备号的申请。将字符设备注册到系统中是通过加载函数中的globalmem_setup_cdev函数来完成的。该函数具体完成的工作可以参考下图:

globalmem_setup_cdev流程图

结合上图,接下来参看globalmem_setup_cdev函数的具体代码。由cdev_init中,除了初始化cdev结构中的字段,最重要的是将globalmem_fops传递给cdev中的ops。

static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
	int ret;
	int devno = MKDEV(globalmem_major, index);

	cdev_init(&dev->cdev, &globalmem_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &globalmem_fops;
	ret = cdev_add(&dev->cdev, devno, 1);
	if(ret){
		printk("adding globalmem error");
	}
}

通过上述的几步,就可以完成字符设备驱动加载函数。对于字符设备卸载函数而言,所作的工作就是加载函数功能的逆向:将cdev从系统中注销;释放设备结构体所占用的内存空间;释放设备号。具体可参看代码:

static void __exit globalmem_exit(void)
{
	/*free struct cdev*/
	cdev_del(&dev->cdev);
	/*free the memory of struct globalmem_dev*/
	kfree(dev);
	/*free the devno*/
	unregister_chrdev_region(MKDEV(globalmem_major,0), 1);
}

3.3.对file_operaions成员函数的实现
最基本的成员函数包括open、release、read和write等函数。对这些函数的具体实现还要根据具体的设备要求来完成。在本文所述的全局内存字符设备驱动中,我们要实现的是功能是在用户程序中对这字符设备中的这块全局内存进行读写操作。读写函数的具体功能可参考下图:

对于open和release可以不做具体实现,当用户态程序打开或释放设备文件时,会自动调用内核中通用的打开和释放函数。

这样,一个基本的字符设备驱动程序就完成了。本文所述实例是一个有代表性的通用模型,可以在理解本程序的基础上继续增加其他功能。

虚拟映射和mmap()

2011年1月12日 由 edsionte 没有评论 »

虚存映射

我们知道,程序是存储在磁盘上到静态文件;进程是对程序到一次运行过程。在进程开始运行时,进程的代码和数据等内容必须装入到进程用户空间到适当区域。这些区域也就是所谓的代码段和数据段等,而被装入的数据和代码等内容被称为进程的可执行映像。从上面都描述中可以发现,进程在运行时并不是将程序一下子就装入到物理内存,而只是将程序装入到进程的用户空间,这个装入的过程称为虚存映射。

一个源程序在成为可执行文件的过程中会经历预处理、编译、汇编和链接四个阶段。因此,进程要成功运行不仅要在其用户空间装入进程映像,也要装入该进程所用到到函数库以及链接程序等。所以,一个进程到用户空间就被分为若干个内存区域。linux使用mm_struct结构来描述一个进程到用户地址空间,使用vm_area_struct结构来描述进程地址空间中的一个内存区域。因此,一个vm_area_struct结构可能代表进程到数据段,也可能代表链接程序到代码段等。

进程的虚存映射所做的只是将磁盘上到文件映射到该进程的用户地址空间,并没有建立虚拟内存到物理内存的映射。当某个可执行映像映射到进程用户空间并开始执行时,只有很少一部分虚拟页被装入了物理内存。在进程后续到执行过程中,如果需要访问到数据并不在物理内存中,则产生一个缺页中断(其实是异常),将所需页从交换区或磁盘中调入物理内存,这个过程即虚拟内存中到请页机制。

进程到虚存区

那么对于一个任意的进程,我们可以通过下面到方法查看其地址空间中到内存区域。

我们先看一个简单的测试程序:

#include < stdio.h >
#include < stdlib.h >

int main()
{
	int i=1;
	char *str=NULL;
	printf("hello,world!\n");
	str=(char *)malloc(sizeof(char)*1119);

	sleep(1000);

	return 0;
}

这个程序中使用到了malloc函数,因此str变量存储于堆中。我们通过打印/proc/3530/maps文件,即可看到该进程的内存空间划分。其中3530是该进程的id。

edsionte@edsionte-desktop:~$ cat /proc/3530/maps
0014a000-00165000 r-xp 00000000 08:07 398276     /lib/ld-2.11.1.so
00165000-00166000 r--p 0001a000 08:07 398276     /lib/ld-2.11.1.so
00166000-00167000 rw-p 0001b000 08:07 398276     /lib/ld-2.11.1.so
001d8000-0032b000 r-xp 00000000 08:07 421931     /lib/tls/i686/cmov/libc-2.11.1.so
0032b000-0032c000 ---p 00153000 08:07 421931     /lib/tls/i686/cmov/libc-2.11.1.so
0032c000-0032e000 r--p 00153000 08:07 421931     /lib/tls/i686/cmov/libc-2.11.1.so
0032e000-0032f000 rw-p 00155000 08:07 421931     /lib/tls/i686/cmov/libc-2.11.1.so
0032f000-00332000 rw-p 00000000 00:00 0
00441000-00442000 r-xp 00000000 00:00 0          [vdso]
08048000-08049000 r-xp 00000000 08:09 326401     /home/edsionte/test
08049000-0804a000 r--p 00000000 08:09 326401     /home/edsionte/test
0804a000-0804b000 rw-p 00001000 08:09 326401     /home/edsionte/test
08958000-08979000 rw-p 00000000 00:00 0          [heap]
b78ce000-b78cf000 rw-p 00000000 00:00 0
b78dd000-b78e0000 rw-p 00000000 00:00 0
bfa6a000-bfa7f000 rw-p 00000000 00:00 0          [stack]

每一行信息依次显示的内容为内存区域其实地址-终止地址,访问权限,偏移量,主设备号:次设备号,inode,文件。

上面的信息不但包含了test可执行对象的各内存区域,而且还分别显示了 /lib/ld-2.11.1.so(动态连接程序)文件和/lib/tls/i686/cmov/libc-2.11.1.so(C库)文件的内存区域信息。

我们从某个内存区域的访问权限上可以大致判断该区域的类型。各个属性符号的意义为:r-read,w-write,x-execute,s-shared,p-private。因此,r-x一般代表程序的代码段,即可读,可执行。rw-可能代表数据段,BSS段和堆栈段等,即可读,可写。堆栈段从行信息的文件名就可以区分;如果某行信息的文件名为空,那么可能是BSS段。另外,上述test进程共享了内核动态库,所以在00441000-00442000行处文件名显示为vdso(Virtual Dynamic Shared Object)。

mmap系统调用

通过mmap系统调用可以在进程到用户空间中创建一个新到虚存区。该系统调用到原型如下:

#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

该函数可以将以打开的文件映射到进程用户空间到一片内存区上,执行成功后,该函数返回这段映射区到首地址。用户得到这片虚存的首地址后,就可以像访问内存那样访问文件。

该系统调用的参数说明如下:

addr:映射到用户地址空间到起始地址;
length:映射区以字节为单位到长度;
prot:对映射区到访问模式。包括PROT_EXEC(可执行),PROT_READ (可读),PROT_WRITE(可写),PROT_NONE(文件不可访问)。这个访问模式不能超过所映射文件到打开模式。比如被映射的文件打开模式为只读,那么此处到访问模式不能是可读写的。
flags:这个字段比较灵活,不同到标志有不同的功能,具体如下:
MAP_SHARED:创建一个可被子进程共享的映射区;
MAP_PRIVATE:创建一个“写实复制”的映射区;
MAP_ANONYMOUS:创建一个匿名到映射区,该虚存区与进程无关;
fd:所要映射到进程用户空间的文件描述符,该文件必须为以打开的文件;
offset:文件的起始映射偏移量;

mmap()举例

在该程序中,首先以只读方式打开文件test.c,再通过该文件返回到文件描述符和mmap函数将test.c文件映射到当前进程到用户地址空间中。成功执行mmap函数后,buf被赋值为所映射的虚存区的首地址。注意,mmap函数返回的是void型指针,而buf是char型指针。将mmap返回值赋值给buf变量时,自动将void*转化为char*型。

最后,就像平常我们使用一个char型指针变量那样,依次打印出buf中到数据。

#include < stdio.h >
#include < sys/mman.h >
#include < fcntl.h >
int main()
{
	int i,fd;
	char *buf = NULL;

	fd = open("./test.c", O_RDONLY);
	if(fd < 0)
	{
		printf("open error\n");
		return -1;
	}

	buf = mmap(NULL, 12, PROT_READ, MAP_PRIVATE ,fd, 0);
	for(i = 0;i < 12;i++)
	{
		printf("%c",buf[i]);
	}
	printf("\n");

	return 0;
}

try一下!

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

2010年12月12日 由 edsionte 2 条评论 »

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(),本文并不涉及涉及此函数的分析。

进程描述符的处理

2010年12月9日 由 edsionte 5 条评论 »

进程描述符的处理

对于每一个进程而言,内核为其单独分配了一个内存区域,这个区域存储的是内核栈和该进程所对应的一个小型进程描述符——thread_info结构。

struct thread_info {
	struct task_struct	*task;		/* main task structure */
	struct exec_domain	*exec_domain;	/* execution domain */
	unsigned long		flags;		/* low level flags */
	unsigned long		status;		/* thread-synchronous flags */
	__u32			cpu;		/* current CPU */
	__s32			preempt_count; /* 0 => preemptable, <0 => BUG */
	mm_segment_t		addr_limit;
	struct restart_block    restart_block;
	unsigned long           previous_esp;
	__u8			supervisor_stack[0];
};

之所以将thread_info结构称之为小型的进程描述符,是因为在这个结构中并没有直接包含与进程相关的字段,而是通过task字段指向具体某个进程描述符。通常这块内存区域的大小是8KB,也就是两个页的大小(有时候也使用一个页来存储,即4KB)。一个进程的内核栈和thread_info结构之间的逻辑关系如下图所示:

从上图可知,内核栈是从该内存区域的顶层向下(从高地址到低地址)增长的,而thread_info结构则是从该区域的开始处向上(从低地址到高地址)增长。内核栈的栈顶地址存储在esp寄存器中。所以,当进程从用户态切换到内核态后,esp寄存器指向这个区域的末端。

从代码的角度来看,内核栈和thread_info结构是被定义在一个联合体当中的:

 //定义在linux/include/linux/sched.h中
 union thread_union {
         struct thread_info thread_info;
         unsigned long stack[THREAD_SIZE/sizeof(long)];
 };

其中,THREAD_SIZE的值取8192时,stack数组的大小为2048;THREAD_SIZE的值取4096时,stack数组的大小为1024。现在我们应该思考,为何要将内核栈和thread_info(其实也就相当于task_struct,只不过使用thread_info结构更节省空间)紧密的放在一起?最主要的原因就是内核可以很容易的通过esp寄存器的值获得当前正在运行进程的thread_info结构的地址,进而获得当前进程描述符的地址。

//定义在/linux/include/asm-i386/thread_info.h中
  static inline struct thread_info *current_thread_info(void)
  {
          struct thread_info *ti;
          __asm__("andl %%esp,%0; ":"=r" (ti) : "0" (~(THREAD_SIZE - 1)));
          return ti;
  }

这条内联汇编语句会屏蔽掉esp寄存器中的内核栈顶地址的低13位(或12位,当THREAD_SIZE为4096时)。此时ti所指的地址就是这片内存区域的起始地址,也就刚好是thread_info结构的地址。但是,thread_info结构的地址并不会对我们直接有用。我们通常可以轻松的通过current宏获得当前进程的task_struct结构,这个宏是如何实现的?

//定义在linux/include/asm-i386/current.h中
   static inline struct task_struct * get_current(void)
   {
          return current_thread_info()->task;
  }
  #define current get_current()

通过上述源码可以发现,current宏返回的是thread_info结构task字段。而task正好指向与thread_info结构关联的那个进程描述符。得到current后,我们就可以获得当前正在运行进程的描述符中任何一个字段了,比如我们通常所做的:current->pid。

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

2010年12月9日 由 edsionte 没有评论 »

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

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