日志标签 ‘系统调用’

open()在Linux内核的实现(1)-基本实现

2015年1月4日

1.基本说明

在用户态使用open()时,必须向该函数传入文件路径和打开权限。这两个参数传入内核后,内核首先检查这个文件路径存在的合法性,同时还需检查使用者是否有合法权限打开该文件。如果一切顺利,那么内核将对访问该文件的进程创建一个file结构。

在用户态,通常open()在操作成功时返回的是一个非负整数,即所谓的文件描述符(fd,file descriptor);并且,用户态后续对文件的读写操作等都是通过fd来完成的。由此可见fd与file结构在内核中有一定的关联。

具体的,内核使用进程描述符task_struct来描述一个进程,而该进程所有已打开文件对应的file结构将形成一个数组files(其为files_struct结构),内核向用户返回的fd便是该数组中具体file结构的索引。默认情况下,每个进程创建后都已打开了标准输入文件、标准输出文件、标准错误文件,因此他们的文件描述符依次为0、1和2。

2.函数分析

2.1.do_sys_open

明白了上述原理,那么open系统调用在内核中的基本实现过程就很清晰。根据系统调用入口函数的命名规则,open系统调用的入口函数应该为sys_open。不过,目前内核统一使用SYSCALL_DEFINEn宏来描述系统调用入口函数,因此可以在open.c文件中找到该入口函数,具体如下所示:

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode);

该函数内部直接调用了do_sys_open函数,具体声明如下:

long do_sys_open(int dfd, const char __user *filename, int flags, int mode);

这个函数的参数基本上与open系统调用的参数一致。

该函数可以简单概括open系统调用的功能:

1.通过build_open_flags()将用户态的flags和mode转换成对应的内核态标志;

2.由于filename是用户态的内存缓冲区(使用了__user修饰),因此通过getname()将文件名从用户态拷贝至内核态;

3.get_unused_fd_flags()为即将打开的文件分配文件描述符;也就是在当前进程的files数组中寻找一个未使用的位置;

4.通过do_filp_open()为文件创建file结构体;

5.如果创建file成功,则通过fd_install()将fd和file进行关联;如果创建file失败,通过put_unused_fd()将已分配的fd返回至系统,并且根据file生成错误的fd;

6.通过putname()释放在内核分配的路径缓冲区;

7.返回fd;

当open系统调用执行完毕后,fd返回用户态,内核态新建了与其关联的file;对于每个进程而言,通过files_struct来记录其所打开的文件,具体通过fd_array数据保存fd和file的对应关系,fd本质为该数组的索引。

3.总结

至此,open的基本实现过程已经分析完毕。不过do_sys_open函数没有直接体现文件路径的查找过程,这部分将是整个open系统调用内核实现的重要部分。如果对此感兴趣,可以继续阅读本系列后续文章。

参考资料:

1.Linux源码3.2.69;

2.Linux系统调用open七日游:http://blog.chinaunix.net/uid-20522771-id-4419666.html

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

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

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

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/;

文件操作函数在VFS层的实现

2012年11月23日

虚拟文件系统(Virtual Filesystem Switch,VFS)为各种文件系统提供了一个通用的接口,它使得上层进程在进行与文件系统相关的操作时可以使用同一组系统调用,但是系统调用在内核中可以根据不同的文件系统执行不同的操作。

与文件相关的基本操作函数有:open、read、write和close,本文将结合内核源代码分析这些函数在虚拟文件系统中的实现。

1.open()的实现

open系统调用的作用是打开或创建一个文件,并且返回该文件的文件描述符。在内核中,open系统调用主要完成的工作是为此次打开的文件创建file对象,该对象在fd_array数组中的索引值为返回用户空间的文件描述符。

open系统调用对应的系统调用服务例程为sys_open,不过目前内核已经统一使用SYSCALL_DEFINEn这种方式对系统调用服务例程进行定义。在open系统调用服务例程中又直接调用了do_sys_open函数,它是打开动作的主体函数。

long do_sys_open(int dfd, const char __user *filename, int flags, int mode)
{
        char *tmp = getname(filename);
        int fd = PTR_ERR(tmp);

        if (!IS_ERR(tmp)) {
                fd = get_unused_fd_flags(flags);
                if (fd >= 0) {
                        struct file *f = do_filp_open(dfd, tmp, flags, mode, 0);
                        if (IS_ERR(f)) {
                                put_unused_fd(fd);
                                fd = PTR_ERR(f);
                        } else {
                                fsnotify_open(f->f_path.dentry);
                                fd_install(fd, f);
                        }
                }
                putname(tmp);
        }
        return fd;
}

用户进程使用open打开文件时将传递文件路径filename,因此该函数第一步先通过getname函数从用户空间读取文件路径到内核空间,暂存到tmp。通过get_unused_fd_flags函数在当前进程的fd_array数据中找到一个何时的位置,并返回其索引。

接下来通过do_filp_open函数执行打开文件的核心操作:根据系统调用中的标志参数flags和访问模式mode设置相应的局部变量以便后续使用;根据要打开文件的路径tmp寻找其inode节点,如果该inode节点不存在并且设置了O_CREATE标志则在磁盘上创建一个新的磁盘索引节点;分配一个新的文件对象,并根据系统调用传递的标志和访问模式设置文件对象的f_flags和f_mode字段;使用索引节点的i_fop字段初始化文件对象的f_op字段;将该文件对象插入到超级块指向的打开文件链表中;如果文件对象操作函数集中的open函数被定义则调用它;最后返回这个文件对象;

如果这个文件对象创建成功,则通过fd_install函数将该文件对象赋值到fd_array数组的第fd个元素中。

2.read()的实现

读文件系统调用read()的作用是根据文件描述符fd读取指定长度size的数据到缓冲区buf中。该系统调用的实现涉及了内核中对I/O进行处理的各个层次,但是对于VFS层来说实现方法比较清晰。

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
        struct file *file;
        ssize_t ret = -EBADF;
        int fput_needed;
        
        file = fget_light(fd, &fput_needed);
        if (file) {
                loff_t pos = file_pos_read(file);
                ret = vfs_read(file, buf, count, &pos);
                file_pos_write(file, pos);
                fput_light(file, fput_needed);
        }

        return ret;
}

在read系统调用对应的服务例程中,首先使用fget_light函数通过fd获取对应的文件对象;再通过file_pos_read函数获取读文件的起始偏移量,即文件对象的f_pos字段的值;接着通过vfs_read函数进行读操作;通过file_pos_write函数更新文件当前的偏移量;通过fput_light函数释放文件对象;最终返回vfs_read函数的返回值ret,该值则为实际读取数据的长度。

read系统服务例程中最核心的函数即为vfs_read,它的主要工作是选择一个具体的读操作函数。如果当前文件对象操作函数集中的read钩子函数(file->f_op->read)被实现(通常在驱动程序中实现),则调用它。否则使用内核默认的读函数do_sys_read。

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
        ssize_t ret;

        if (!(file->f_mode & FMODE_READ))
                return -EBADF;
        if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))
                return -EINVAL;
        if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
                return -EFAULT;

        ret = rw_verify_area(READ, file, pos, count);
        if (ret >= 0) {
                count = ret;
                if (file->f_op->read)
                        ret = file->f_op->read(file, buf, count, pos);
                else
                        ret = do_sync_read(file, buf, count, pos);
                if (ret > 0) {
                        fsnotify_access(file->f_path.dentry);
                        add_rchar(current, ret);
                }
                inc_syscr(current);
        }

        return ret;
}

事实上,do_sys_read函数在内部调用钩子函数aio_read(file->f_op->aio_read),该钩子函数一般指向内核实现的通用读函数generic_file_aio_read。这个通用函数已经不属于我们本文所述的VFS层的实现范畴。

3.write函数的实现

write系统调用在VFS层的实现流程与read类似,只不过在出现read的地方将其相应的置换为write。

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
                size_t, count)
{
        struct file *file;
        ssize_t ret = -EBADF;
        int fput_needed;

        file = fget_light(fd, &fput_needed);
        if (file) {
                loff_t pos = file_pos_read(file);
                ret = vfs_write(file, buf, count, &pos);
                file_pos_write(file, pos);
                fput_light(file, fput_needed);
        }

        return ret;
}

当然最终实现写文件操作的函数也是file->f_op->write或者内核中通用的写操作generic_file_aio_write。

4.close()的实现

close系统调用对应的服务例程中,它首先通过fd在文件对象数组中获取文件对象,接着则将fd处的文件对象清空。接下来的大部分工作都通过filp_close函数完成,它主要的工作是调用flush钩子函数将页高速缓存中的数据全部写回磁盘,释放该文件上的所有锁,通过fput函数释放该文件对象。最后返回0或者一个错误码。

libc库和系统调用

2012年6月2日

Linux系统调用这部分经常出现两个词:libc库和封装函数,不知道你是否清楚它们的含义?

libc

libc是Standard C library的简称,它是符合ANSI C标准的一个标准函数库。libc库提供C语言中所使用的宏,类型定义,字符串操作函数,数学计算函数以及输入输出函数等。正如ANSI C是C语言的标准一样,libc只是一种函数库标准,每个操作系统都会按照该标准对标准库进行具体实现。通常我们所说的libc是特指某个操作系统的标准库,比如我们在Linux操作系统下所说的libc即glibc。glibc是类Unix操作系统中使用最广泛的libc库,它的全称是GNU C Library。

类Unix操作系统通常将libc库作为操作系统的一部分,它被视为操作系统与用户程序之间的接口。libc库不仅实现标准C语言中的函数,而且也包含自己所属的函数接口。比如在glibc库中,既包含标准C中的fopen(),又包含类Unix系统中的open()。在类Unix操作系统中,如果缺失了标准库,那么整个操作系统将不能正常运转。

与类Unix操作系统不同的是,Windows系统并不将libc库作为整个核心操作系统的一部分。通常每个编译器都附属自己的libc库,这些libc既可以静态编译到程序中,又可以动态编译到程序中。也就是说应用程序依赖编译器而不是操作系统。

封装函数

在Linux系统中,glibc库中包含许多API,大多数API都对应一个系统调用,比如应用程序中使用的接口open()就对应同名的系统调用open()。在glibc库中通过封装例程(Wrapper Routine)将API和系统调用关联起来。API是头文件中所定义的函数接口,而位于glibc中的封装例程则是对该API对应功能的具体实现。事实上,我们知道接口open()所要完成的功能是通过系统调用open()完成的,因此封装例程要做的工作就是先将接口open()中的参数复制到相应寄存器中,然后引发一个异常,从而系统进入内核去执行sys_open(),最后当系统调用执行完毕后,封装例程还要将错误码返回到应用程序中。

需要注意的是,函数库中的API和系统调用并没有一一对应的关系。应用程序借助系统调用可以获得内核所提供的服务,像字符串操作这样的函数并不需要借助内核来实现,因此也就不必与某个系统调用关联。

不过,我们并不是必须通过封装例程才能使用系统调用,syscall()和_syscallx()两个函数可以直接调用系统调用。具体使用方法man手册中已经说明的很清楚了。

参考:

1. http://en.wikipedia.org/wiki/Libc

2. man syscalls

内核任务

2012年5月31日

内核任务是指内核中执行的一切活动对象,每个内核任务都拥有一个独立的程序计数器、栈和一组寄存器。我们可以将Linux内核看作是不断对请求进行响应的服务器,这里的请求可能来自CPU上正在执行的进程,也可能是来自发出中断请求的外部设备。这个类比用来强调内核中各个任务之间并不是严格按照顺序执行的,而是采用交错执行的方式。本文简单说明内核中的任务分类,以及每种内核任务的特点。

内核线程

内核线程只运行在内核空间,它为内核完成一些周期性任务,比如用于执行工作队列的keventd线程,用于执行内存回收的kswapd线程以及用于将脏缓冲区中的内容刷新到磁盘上的pdflush线程等。内核线程与用户进程在某些方面比较类似,比如它们被内核中同一个进程调度器所调度、都通过do_fork()创建等。这主要是因为内核线程与进程都通过进程描述符task_struct来描述,我们可以将内核线程看作是运行在内核空间的进程。内核中使用thread来描述内核线程是因为它并不拥有用户空间,因此内核线程之间的切换是迅速的。虽然都使用了线程一词,但是应该和用户态的线程有所区分。

系统调用

用户态进程必须通过系统调用才能进入内核并获得内核提供的服务,比如访问硬件设备。系统调用是一种异常,它通过128号中断向量向内核发出一个明确的请求。系统一旦执行某个系统调用,就从用户态切换到内核态,接下来内核就代表发出系统调用的进程执行,执行完毕后又返回用户空间。由于系统调用是内核代表进程执行,因此它可以获得当前进程的信息,比如可以访问当前进程的描述符中的信息,而且系统调用的执行使用的是当前发出系统调用进程的时间片。

中断处理程序

根据Intel的说法,异步中断也被简称为中断,它是由硬件设备依照CPU时钟信号随机产生的。这类中断可以打断任何正在执行的内核任务。在响应一个特定中断的时候,内核会执行一个函数,该函数称为中断处理程序(Interrupt Handler)。如果一条中断线上共享了多个设备,那么每个设备将对应一个中断服务例程(Interrupt Server Routine,ISR)。当该某个中断发生时,内核会调用相应的中断处理程序。该中断处理程序首先在内核态堆栈中保存IRQ和寄存器的值,然后响应该中断,接着执行共享这个中断线上所有设备的中断服务例程,执行完毕后恢复之前被打断的内核任务执行现场。

异常处理程序

同步中断也被称为异常,它是由CPU控制单元产生的,只有在一条指令终止执行后CPU才会发出异常。内核为每一种异常提供了一个专门的异常处理程序。异常处理程序的执行过程与中断处理程序类似,它首先将大多数寄存器的值压入内核堆栈中,接着调用响应异常处理程序,最后从异常处理程序中返回并向产生异常的进程发出一个信号。

可延迟函数

中断随时可能发生,因此中断处理程序也就随时会被执行。为了能尽快恢复被中断的代码再次执行,中断处理程序必须快速运行完毕。但是,中断处理程序极有可能和硬件设备进行交互,它需要花一些时间和硬件进行数据交互,比如等待数据的到来,从外设拷贝数据到内存等。中断程序既要快速运行,又要完成大量工作,这两者显然存在矛盾。为了解决这个问题,内核将中断处理的过程分为上下两部分,上部分即中断处理程序,主要对中断请求进行快速响应;下半部分主要完成中断处理过程中对时间要求相对宽松的工作。下半部分包含三种机制:软中断(softirq,与上文中软件中断不同),tasklet以及工作队列。鉴于工作队列通过keventd内核线程来执行,因此将软中断和tasklet统称为可延迟函数。

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