日志标签 ‘内核’

open()在Linux内核的实现-准备工作

2015年1月4日

1.基本说明

“open()在Linux内核的实现”系列文章将分析open系统调用在Linux内核中的实现过程。本系列文章分为六篇,每篇文章都描述 open()实现的一部分内容,与前后的系列文章保持相对独立。本文属于前序文章,集中说明后续文章涉及到的基本原理和基本数据结构,并且对整个分析过程进行Q&A。

本系列文章参考Linux内核源码版本为3.2.69。

2.数据结构

dentry结构

对于打开文件这个操作来说,它是通过路径名查找对应文件inode的过程,这里用户直面的是文件路径,而内核关注的inode。在文件路径和inode之间通过目录项(dentry)缓存进行关联,dentry缓存加快了vfs对文件的查找。所有的目录项通过散列表进行组织,这样可以快速对dentry进行查找;此外,内核将常用的dentry通过LRU算法进行组织,这样可以快速查到最近一段时间经常使用的dentry。

下面将对dentry中的部分字段进行说明。

d_inode:该字段指向目录项所关联的文件。如果该字段为空,则说明当前目录项指向的是一个并不存在的文件。

d_name:该字段表示目录项名称(并不是整个路径名),但它并不是单纯的字符串,而是将字符串文件名、字符串长度和散列值封装成qstr(quick string)结构,这样可以加速目录项的查找工作;

d_iname:当目录项名称长度小于DNAME_INLINE_LEN时,则该字符串名称则直接通过该字段进行存储;

d_parent:一个路径中的目录项形成层级结构。该字段指向当前目录项的父目录dentry实例;特别的,对于根目录项来说,这个字段指向自己;

d_subdirs:当前目录项如果代表目录,则该目录下的所有文件对应的dentry将形成d_subdirs链表(表头);

d_child:这个字段是父目录dentry中d_subdirs链表中的结点;

d_alias:一个文件可能有多个名称(硬链接),即多个dentry,则一个文件的所有目录项则形成一个链表,这个链表头位于该文件inode中的i_dentry字段,d_alias充当的该链表中的结点;

vfsmount结构

每个挂载在内核目录树中的文件系统都将对应一个vfsmount结构,下面将对该结构中的部分字段进行说明。假设设备/dev/sdc为ntfs文件系统,现需要将其挂载在文件系统为ext3的/home/edsionte/work下。因此,/home/edsionte/work可以被称为ntfs文件系统的挂载点,并且称ntfs文件系统与ext3文件系统形成父子文件文件系统关系。同时ntfs也可称为源文件系统,而ext3也可称为目的文件系统。

mnt_hash:内核将系统内所有已挂载的文件系统通过散列表的形式进行组织,每个vfsmount将处于其对应哈希值的冲突链表当中。mnt_hash字段则为具体冲突链表的元素。

mnt_mounts:如果当前文件系统下挂载了其他的子文件系统,那么这些子文件系统将通过自身vfsmount中的mnt_child字段组成一个链表,该链表头为父文件系统中的mnt_mounts字段。

mnt_child:当前文件系统将通过该字段与其他父文件系统下的子文件系统组成一个链表。

mnt_parent:该字段指向父文件系统对应的vfsmount结构。即指向ext3文件系统对应的vfsmount结构。

mnt_mountpoint:该字段表示源文件系统在目的文件系统中挂载点对应的dentry结构。/home/edsionte/work为挂载点,则该字段指向目录项work。

mnt_root:指向当前文件系统的根目录项。对于源文件系统ntfs来说,根目录项相对为/,但在整个系统目录树中,根目录项为work。

mnt_sb:每个文件系统都将对应一个super_block结构,该字段指向/dev/sdc设备上文件系统对应的超级块。

mnt_list:所有处于一个名字空间的文件系统通过mnt_list字段链接在一起,而该链表的表头为该名字空间结构中的list字段。

mnt_ns:该字段表示当前vfsmount所对应的名字空间结构。

nameidata结构

文件路径是由各级别的目录项组成的,因此路径的查找过程是对目录项的逐级查找。nameidata结构是路径查找过程中的核心数据结构,在每一级目录项查找过程中,它向查找函数输入参数,并且保存本次查找的结果,因此它是不断变化的。

下面对nameidata结构中的部分字段进行说明。

path:该字段用于保存当前目录项。该字段是path结构,该结构将目录项和该目录项所关联的vfsmount结构进行封装。

last:该字段为qstr结构,表示当前目录项的名称。

root:该字段为path结构,表示根目录。

last_type:表示当前目录项的类型。

inode:表示当前目录项对应的inode,它的取值来自于path.dentry.d_inode。

depth:表示符号链接当前的嵌套级别,最大不能超过MAX_NESTED_LINKS;

saved_names:该字符串数组表示符号链接每个嵌套级别的名称;

目录项的类型包括以下几种情况:

LAST_NORM:普通目录项;

LAST_ROOT:当前目录项为/;

LAST_DOT:当前目录项为.;

LAST_DOTDOT:当前目录项为..;

LAST_BIND:当前目录项为符号链接文件;

3.基本原理

rcu机制

写时拷贝(rcu,Read-Copy-Update)是Linux内核的一种锁机制,它是一种改良的rwlock(但并不能代替),适合读者多写者少的情景,可以保证读写者操作同时进行。

对于读者而言,rcu机制可以保证多个读者在不申请锁的情况下直接对临界区资源进行访问。对于写者而言,它之所以可以与读者同时访问共享资源,是因为在读者读取原始数据的同时它修改的是原始数据的备份。当所有读者都退出访问该共享资源时,写着将用修改后的新数据替换原始数据。同时,rcu中的回收机制将对原始数据进行回收。

与rwlock相比,在读多写少的情况下,rcu的效率会高很多。因为rcu所提供的拷贝技术使读写者可以同时访问共享资源,因此免去了读写者申请锁时所花费的开销。

由于rcu机制的自身特点,它所使用的上下文必须是不可睡眠的。因为,写者在替换原始数据之前会等待所有读者退出临界区,而此时如果读者处于阻塞状态,那么系统将进入死锁状态。

rcu-walk和ref-walk

内核中的路径查找提供两种模式:ref-walk和rcu-walk。前者是内核中传统的路径查找方式,而ref-walk是基于rcu所机制的一种路径查找模式。由于路径查找正好是一个读多写少的情景,基于rcu机制快速高效的特点,该模式可以高效的进行路径查找。不过,rcu-walk并不是万能的,如果路径查找过程中需要睡眠,那么必须将查找模式由rcu-walk切换到ref-walk。

4.总结

本篇对open()在内核实现中所涉及的数据结构和原理进行实现说明,并且针对open()实现过程的一些问题进行Q&A。可以在阅读open()内核源码之前阅读本文,也可在阅读之后再次阅读本文。

参考资料:

1.Linux源码3.2.69;

2.深入理解Linux内核:http://book.douban.com/subject/2287506/;

3.深入Linux内核架构:http://book.douban.com/subject/4843567/;

4.Linux内核探秘:http://book.douban.com/subject/25817503/;

Linux内核文件系统挂载分析

2014年2月25日

本文将针对内核版本3.2.0中的mount系统调用实现过程进行简单说明。

1.数据结构

下面将对文件系统挂载过程中涉及到的两个主要数据结构vfsmount和path进行节本说明。

1.1 struct vfsmount

每个挂载在内核目录树中的文件系统都将对应一个vfsmount结构,下面将对该结构中的部分字段进行说明。假设设备/dev/sdc为ntfs文件系统,现需要将其挂载在文件系统为ext3的/home/edsionte/work下。因此,/home/edsionte/work可以被称为ntfs文件系统的挂载点,并且称ntfs文件系统与ext3文件系统形成父子文件文件系统关系。同时ntfs也可称为源文件系统,而ext3也可称为目的文件系统。

struct list_head mnt_hash;

内核将系统内所有已挂载的文件系统通过散列表的形式进行组织,每个vfsmount将处于其对应哈希值的冲突链表当中。mnt_hash字段则为具体冲突链表的元素。

struct list_head mnt_mounts;

如果当前文件系统下挂载了其他的子文件系统,那么这些子文件系统将通过自身vfsmount中的mnt_child字段组成一个链表,该链表头为父文件系统中的mnt_mounts字段。

struct list_head mnt_child;

当前文件系统将通过该字段与其他父文件系统下的子文件系统组成一个链表。

struct vfsmount *mnt_parent;

该字段指向父文件系统对应的vfsmount结构。即指向ext3文件系统对应的vfsmount结构。

struct dentry *mnt_mountpoint;

该字段表示源文件系统在目的文件系统中挂载点对应的dentry结构。/home/edsionte/work为挂载点,则该字段指向目录项work。

struct dentry *mnt_root;

指向当前文件系统的根目录项。对于源文件系统ntfs来说,根目录项相对为/,但在整个系统目录树中,根目录项为work。

struct super_block *mnt_sb;

每个文件系统都将对应一个super_block结构,该字段指向/dev/sdc设备上文件系统对应的超级块。

struct list_head mnt_list;

所有处于一个名字空间的文件系统通过mnt_list字段链接在一起,而该链表的表头为该名字空间结构中的list字段。

struct mnt_namespace *mnt_ns;

该字段表示当前vfsmount所对应的名字空间结构。

1.2 struct path

path结构由vfsmount结构和dentry结构组成。该结构在挂载文件系统时表示目的文件系统的vfsmount结构和挂载点dentry。

2.函数调用关系图

do_mount

3.实现

3.1 mount系统调用服务例程

mount()系统调用服务例程为:

SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name, char __user *, type, unsigned long, flags, void __user *, data)

其内部实现主要是将用户态的参数依次复制到内核态,接着调用内核函数do_mount()。

3.2 do_mount()

该函数内部首先通过kern_path()获取目的文件系统的path结构,即挂载点目录项以及目的文件系统的vfsmount结构;接着,通过检查flags对挂载操作进行不同目的的分发。这里我们只讨论最普通的情形,即将一个文件系统挂载在一个新的挂载点中,这种情况调用do_new_mount()。

3.3 do_new_mount()

这个函数描述的是挂载一个新文件系统最普遍的情形,主要包括以下几点:

1.文件系统类型、操纵权限检查等;

2.通过do_kern_mount()获取源文件系统的vfsmount结构;

3.通过do_add_mount()将源文件系统增加到目的文件系统中;

3.4 do_add_mount()

1.flags参数合法性检查;

2.检查指定的目的文件系统是否为当前文件系统。如果是,则失败;

3.检查源文件系统的根inode是否为链接文件。如果是,则失败;

4.通过graft_tree()将源文件系统装载到目的文件系统中。其内部graft又封装了attach_recursive_mnt();

3.5 attach_recursive_mnt()

该函数的主要作用是设置父子文件系统的映射关系。具体操作为:

1.通过mnt_set_mountpoint()将子vfsmount中的mnt_parent指向父vfsmount,将子vfsmount的mnt_mountpoint指向位于父文件系统中的挂载点dentry;

2.通过commit_tree()将子文件系统添加到内核的文件系统哈希表中,并将子文件系统添加到父文件系统对应的子文件系统链表中;

3.6 commit_tree()

1.将当前文件系统的名字空间设置为父名字空间,父vfsmount通过当前vfsmount中的mnt_parent获取;再将其连接到父名字空间链表中。

2.将当前vfsmount加入到对应哈希值的冲突链表当中,哈希值通过hash()计算。其中,mnt_hash作为链表元素。

3.将当前vfsmount加入到父vfsmount对应的子文件系统链表mnt_mounts中。其中,mnt_child作为链表元素。

从整个挂载的处理流程上看,挂载的本质就是将源文件系统的vfsmount结构连接到目的文件系统对应的vfsmount结构中,即具体涉及到两个vfsmount中字段的指向问题。两个vfsmount具体父子等级关系,这也对应着内核中目录树的父子等级关系。

参考资料:

1.深入理解Linux内核:http://book.douban.com/subject/2287506/;

2.深入Linux内核架构:http://book.douban.com/subject/4843567/;

3.Linux内核探秘:http://book.douban.com/subject/25817503/;

Linux内存管理实践-虚拟地址转换物理地址

2011年11月1日

Linux内核中采用了通用的四级分页模型,这种模型不仅适合32位系统也适合64位系统。分页单元是MMU(内存管理单元)中的一部分,它将线性地址转换为物理地址。本文通过一个内核模块程序模拟内核中虚拟地址转换为物理地址的过程,有关分页机制的原理可以参见这里的文章。

static void get_pgtable_macro(void)
{
	printk("PAGE_OFFSET = 0x%lx\n", PAGE_OFFSET);
	printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);
	printk("PUD_SHIFT = %d\n", PUD_SHIFT);
	printk("PMD_SHIFT = %d\n", PMD_SHIFT);
	printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);

	printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);
	printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);
	printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);
	printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);

	printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);
}

static unsigned long vaddr2paddr(unsigned long vaddr)
{
	pgd_t *pgd;
	pud_t *pud;
	pmd_t *pmd;
	pte_t *pte;
	unsigned long paddr = 0;
        unsigned long page_addr = 0;
	unsigned long page_offset = 0;

	pgd = pgd_offset(current->mm, vaddr);
	printk("pgd_val = 0x%lx\n", pgd_val(*pgd));
	printk("pgd_index = %lu\n", pgd_index(vaddr));
	if (pgd_none(*pgd)) {
		printk("not mapped in pgd\n");
		return -1;
	}

	pud = pud_offset(pgd, vaddr);
	printk("pud_val = 0x%lx\n", pud_val(*pud));
	if (pud_none(*pud)) {
		printk("not mapped in pud\n");
		return -1;
	}

	pmd = pmd_offset(pud, vaddr);
	printk("pmd_val = 0x%lx\n", pmd_val(*pmd));
	printk("pmd_index = %lu\n", pmd_index(vaddr));
	if (pmd_none(*pmd)) {
		printk("not mapped in pmd\n");
		return -1;
	}

	pte = pte_offset_kernel(pmd, vaddr);
	printk("pte_val = 0x%lx\n", pte_val(*pte));
	printk("pte_index = %lu\n", pte_index(vaddr));
	if (pte_none(*pte)) {
		printk("not mapped in pte\n");
		return -1;
	}

	//页框物理地址机制 | 偏移量
	page_addr = pte_val(*pte) & PAGE_MASK;
	page_offset = vaddr & ~PAGE_MASK;
	paddr = page_addr | page_offset;
	printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);
        printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);

	return paddr;
}

static int __init v2p_init(void)
{
	unsigned long vaddr = 0;

	printk("vaddr to paddr module is running..\n");
	get_pgtable_macro();
	printk("\n");

	vaddr = (unsigned long)vmalloc(1000 * sizeof(char));
	if (vaddr == 0) {
		printk("vmalloc failed..\n");
		return 0;
	}
	printk("vmalloc_vaddr=0x%lx\n", vaddr);
	vaddr2paddr(vaddr);

	printk("\n\n");
	vaddr = __get_free_page(GFP_KERNEL);
	if (vaddr == 0) {
		printk("__get_free_page failed..\n");
		return 0;
	}
	printk("get_page_vaddr=0x%lx\n", vaddr);
	vaddr2paddr(vaddr);

	return 0;
}

static void __exit v2p_exit(void)
{
	printk("vaddr to paddr module is leaving..\n");
        vfree((void *)vaddr);
        free_page(vaddr);
}

整个程序的结构如下:

1.get_pgtable_macro()打印当前系统分页机制中的一些宏。

2.通过vmalloc()在内核空间中分配内存,调用vaddr2paddr()将虚拟地址转化成物理地址。

3.通过__get_free_pages()在内核空间中分配页框,调用vaddr2paddr()将虚拟地址转化成物理地址。

4.分别通过vfree()和free_page()释放申请的内存空间。

vaddr2paddr()的执行过程如下:

1.通过pgd_offset计算页全局目录项的线性地址pgd,传入的参数为内存描述符mm和线性地址vaddr。接着打印pgd所指的页全局目录项。

2.通过pud_offset计算页上级目录项的线性地址pud,传入的参数为页全局目录项的线性地址pgd和线性地址vaddr。接着打印pud所指的页上级目录项。

3.通过pmd_offset计算页中间目录项的线性地址pmd,传入的参数为页上级目录项的线性地址pud和线性地址vaddr。接着打印pmd所指的页中间目录项。

4.通过pte_offset_kernel计算页表项的线性地址pte,传入的参数为页中间目录项的线性地址pmd和线性地址vaddr。接着打印pte所指的页表项。

5.pte_val(*pte)先取出页表项,与PAGE_MASK相与的结果是得到要访问页的物理地址;vaddr&~PAGE_MASK用来得到线性地址offset字段;两者或运算得到最终的物理地址。

6.打印物理地址。

Ubuntu下编译3.0.4内核

2011年9月8日

Linux内核3.0版本发布已有一段时间了,不知道这个版本号大跃进的内核是否好用。目前各个发行版的linux还仍未采用3.0的内核,因此可以自己动手编译内核来感受一下!趁着这次编译内核的机会可以再熟悉一下编译内核的步骤。

1.下载并解压内核到任意目录

从源码官网下载最新的内核源码3.0.4,可以解压至任意目录,我放在主目录下:

~$ tar xjvf linux-3.0.4.tar.bz2

2.配置内核

对内核进行配置是为了得到内核配置文件.config。通过对内核进行配置,可以使未来编译成功的内核增加或减少对一些内核特性的支持。对内核进行配置有多种方法,有基于文本的配置方式也有基于图形的用户界面。下面采用使用比较广泛的make menuconfig方式:

~/linux-3.0.4$sudo apt-get install libncurses5-dev
~/linux-3.0.4$sudo make menuconfig

由于该配置方式基于ncurses库,所以在启动配置界面前要先安装ncurses库。启动配置界面前,必须进入源码根目录,配置界面启动成功后如下图:

我们这里对内核按照默认的配置方式进行编译,因此当配置菜单启动后直接退出并保存即可。此时就在内核源码根目录下生成了.config文件。

3.编译

编译内核包含两部分的工作,其一是编译内核,即编译配置选项中标记为Y的那部分,这部分内核最终形成bzIamge镜像文件;其二是编译内核模块,即编译配置选项中标记为M的那部分内核,这部分形成以.ko结尾的内核模块目标文件。

上述两部分编译工作可以依次通过make bzImage和make modules完成,也可以通过一条make命令直接完成。编译内核的整个过程比较漫长,因此可以对make加-j参数来提高编译的效率。在make时使用该选项会为编译过程分配n个并发任务,这样可以缩短编译时间。n的取值为cpu个数的二倍。

~/linux-3.0.4$sudo make -j4

4.安装

安装过程分为两部分,首先对内核模块进行安装,这个过程会将刚刚编译内核模块时生成的内核模块复制到/lib/modules/3.0.4/目录下,其中3.0.4为对应的内核版本。使用的命令如下:

~/linux-3.0.4$sudo make modules_install

接着使用下述命令安装编译好的内核:

~/linux-3.0.4$sudo make install

安装内核的过程主要完成了以下的工作:

1.将编译内核时生成的内核镜像bzImage拷贝到/boot目录下,并将这个镜像命名为vmlinuz-3.0.4。如果使用x86的cpu,则该镜像位于arch/x86/boot/目录下(处于正在编译的内核源码下)。

2.将~/linux-3.0.4/目录下的System.map拷贝到/boot/目录下,重新命名为System.map-3.0.4。该文件中存放了内核的符号表。

3.将~/linux-3.0.4/目录下的.config拷贝到/boot/目录下,重新命名为config-3.0.4。

5.创建initrd.img文件

initrd.img即为初始化的ramdisk文件,它是一个镜像文件,将一些最基本的驱动程序和命令工具打包到镜像文件里。该镜像文件的作用是在系统还没有挂载根分区前,系统需要执行一些操作,比如挂载scsi驱动,此时将initrd文件释放到内存中,作为一个虚拟的根分区,然后执行相关脚本,运行insmod命令加载需要的模块。

具体的创建方法如下:

~/linux-3.0.4$sudo mkinitramfs 3.0.4 -o /boot/initrd.img-3.0.4

6.更新grub

最后一步则是更新grub启动菜单,使用下面的命令则可以自动更新启动菜单:

sudo update-grub2

这样会将刚才编译好的内核放在启动菜单的首位,如果需要修改启动菜单中默认系统的启动顺序,则修改/boot/grub/grub.cfg文件中的set default=的值即可。

OK,内核编译完毕。

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

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