审计系统在内核的实现-基本框架

2015年7月3日 由 edsionte 没有评论 »

审计系统在内核的实现部分主要包括是审计内核模块、kauditd内核线程、进程审计和文件系统的审计。审计系统的内核态子系统不仅要能够响应用户态所发送的请求(比如增加或删除审计规则),并且还要在内核发生审计规则所指定的事件时记录并生成日志。

本文将对审计系统在Linux内核中的实现做简单说明,重点说明审计内核模块和kuaditd内核线程的基本实现。

1.审计内核模块的实现

审计内核模块是审计系统在内核的主体程序,它主要负责响应用户态所发送的请求。比如,用户态的auditctl命令用于获取审计系统的状态,以及增加或删除审计规则,该命令对数据的获取和设置是与审计模块进行数据交互而实现的。

1.1.基本实现

1.1.1.audit_init()

审计在内核中的实现包括许多方面,不仅包括审计内核模块,还包括分布在进程、文件等子系统中的实现。审计内核模块是审计系统在内核中的主体框架,它不仅对审计系统中所需的数据结构进行了初始化,而且主要实现对用户态请求的处理。

审计模块的初始化函数为audit_init,它的主要过程如下:

1.首先通过netlink_kernel_create()创建netlink套接字audit_sock,并且注册了(针对用户态)数据的处理函数audit_receive,它将接收并处理用户态发送的netlink数据包;

2.初始化审计系统所需的链表和变量等内容,其中audit_skb_queue链表用于保存审计套接字缓冲区;

3.通过audit_log()生成本次初始化对应的审计日志;

4.初始化基于inode的审计规则所对应的哈希链表;

1.1.2.audit_receive()

audit_receive内部封装了audit_receive_skb函数,该函数的实现过程如下:

1.netlink套接字所发送的数据报文格式为nlmsghdr结构的包头+指定格式的净荷数据;因此,该函数首先按照协议取数据报头和整个数据报文长度,交由audit_receive_msg函数处理;

2.如果数据包处理完成,通过netlink_ack函数向用户态发送响应;

3.通过NLMSG_NEXT获取下一个数据报;

1.1.3.audit_receive_msg()

该函数实际上是一个消息分发处理器,根据数据报头部的nlmsg_type进行消息分发处理。具体的过程如下:

1.从netlink报文头部获取当前消息类型msg_type;

2.通过audit_netlink_ok函数检查当前的消息类型是否合法;如不合法,则退出;

3.检查kauditd内核线程是否已启动,如果没有启动则通过kthread_run()启动该线程;kauditd线程用于将内核中的审计日志发送到用户态中;

4.根据当前的套接字缓冲区skb和当前进程current获取审计所需的数据,比如pid、uid和loginuid等信息;

5.根据消息类型进行消息分发和处理;

1.2.日志的记录

审计系统在内核中通过audit_buffer结构表示一个审计日志缓冲区,它的结构如下:

struct audit_buffer {
struct list_head list;
struct sk_buff *skb; /* formatted skb ready to send */
struct audit_context *ctx; /* NULL or associated context */
gfp_t gfp_mask;
};

list为链表节点;所有的审计日志缓冲区通过链表连接;

skb为套接字缓冲区;由于审计系统通过netlink机制实现用户态和内核态的数据交互,因此需求封装套接字缓冲区;

ctx表示审计上下文;当审计系统进行进程监控时,ctx将保存进程的相关信息;

gfp_mask是内核申请内存时的标志;表示为audit_buffer在哪片内存区申请内存;

内核通过一组接口对审计日志缓冲区进行操作。当审计事件发生时,通过audit_log_start()为该条日志分配audit_buffer结构的缓冲区;根据具体的审计事件,audit_log_vformat()将审计事件发生时要显示的数据以格式化字符串的形式写入缓冲区内;最终,audit_log_end()将缓冲区(审计缓冲区audit_buffer)加入缓冲区(审计套接字缓冲区sk_buffer)链表audit_skb_queue中。链表audit_skb_queue中的缓冲区由内核线程kauditd发送至用户态auditd进程。

1.2.1.audit_log_start()

该函数主要用于分配日志缓冲区,但是如果当前缓冲区链表中的太多,将阻塞当前进程。具体的步骤如下:

1.通过audit_filter_type()检查当前日志类型是否被指定为过滤,如果是,则直接返回;

2.如果当前缓冲区列表audit_skb_queue中存在的节点数目大于指定的audit_backlog_limit大小,则当前的进程将会被阻塞;即创建等待队列实例,改变当前进程的状态为TASK_UNINTERRUPTIBLE,并且将其加入到等待队列audit_backlog_wait中;此时,再次检查缓冲区列表中的数据是否大于所指定的audit_backlog_limit,如果是,则主动发起重新调度;否则,将删除刚才创建的等待队列实例,并且恢复进程的状态;

3.此时,通过audit_buffer_alloc()创建审计缓冲区,并且在该缓冲区中写入审计序列号和时间戳等信息;

当进程在该函数中被阻塞时,将由kauditd内核线程将其唤醒;由于当前进程被阻塞的理由是缓冲区未被发送的数据超过了限制,因此一旦kauditd发送了数据后,该进程将被唤醒。

1.2.2.audit_log_vformat()

审计数据不是一次全部产生,因此audit_log_vformat()可能被调用多次,在需要记录信息的地方调用该函数即可;所有的数据将以格式化字符串的形式进行记录;

1.2.3.audit_log_end()

该函数将结束本次审计日志的记录过程,具体的过程如下:

1.通过audit_rate_check()检查当前审计日志发送的频率是否超过了设定的限制;即如果每秒从内核向用户态发送的日志数超过了制定的数值audit_rate_limit,那么将抛弃当前的日志;

2.从当前审计日志缓冲区中提取所封装的套接字缓冲区,将其加入到audit_skb_queue链表中;

3..唤醒kauditd_wait等待队列中的一个进程。该等待队列由内核线程kauditd控制,该线程将依次发送audit_skb_queue链表中的缓冲区,如果已经无节点可发,则阻塞当前线程;此时,由于刚向audit_skb_queue中加入了新的节点,因此唤醒kauditd线程;

4.释放在audit_log_start()中申请的audit_buffer缓冲区;

1.3.审计规则的操作

audit_receive_msg函数接收并处理用户态auditctl命令发送的消息,这里针对审计规则的操作实现做简单说明。

审计规则的操作包括增加、删除和列举当前规则,这三种操作类型在内核中分别对应为AUDIT_ADD_RULE、AUDIT_DEL_RULE和AUDIT_LIST_RULES。这些操作在audit_receive_msg函数中均由audit_receive_filter函数进行具体处理,具体的过程如下:

1.对于增加和删除操作,先通过audit_log_common_recv_msg()针对本次审计规则的操作进行审计日志缓冲区的分配和部分字段的记录;

2.审计规则在内核中通过双链表进行组织,因此对审计规则的操作即是对链表的操作;

对于AUDIT_LIST_RULES操作,audit_receive_filter函数即分配缓冲区,依次遍历规则并拷贝至缓冲区,最终通过audit_send_list线程将缓冲区数据发送至内核态;具体的,netlink中从内核态发送数据至用户态通过netlink_unicast()函数完成;

对于AUDIT_ADD_RULES操作,audit_receive_filter()先通过audit_data_to_entry()将用户态发送的规则data转换成内核中使用的规则entry,其次将当前的规则添加到对应的规则链表中;由于审计系统的文件监控基于fsnotify机制完成,因此还需将规则中对应的文件加入到fsnotify监控列表中;

AUDIT_DEL_RULES操作的实现与增加是相反过程,在次不再赘述;

2.kauditd内核线程的实现

kauditd内核线程对应的执行函数为kauditd_thread(),它将审计套接字缓冲区链表audit_skb_queue中的节点发送至用户态,用户态auditd将这些日志写入审计文件(auditd.log)。该线程将循环发送缓冲区数据,当缓冲区链表有节点时就利用netlink套接字从内核发送数据到用户态,当该链表中为空时,该线程将进入等待状态,直到缓冲区有新的节点时候再被唤醒。kauditd每次具体的执行过程如下:

1.当用户态的auditd运行(内核中audit_pid标志),且审计功能随启动开启时(内核中audit_default标志),将不断将audit_skb_hold_queue链表中的节点发送至用户态;该链表中通常保存的是当auditd停止后或启动前所产生的审计日志,这些日志在auditd未正常运行时可能已经被发送至系统的syslog中,当auditd正常运行时则需要将它们保存在审计的日志文件中;

2.通过skb_dequeue()在audit_skb_queue链表中移除一个缓冲区节点;

3.唤醒audit_backlog_wait等待链表中的一个进程,表示此时可以继续生成日志了;

4.当在audit_skb_queue中移除节点成功时,将通过kauditd_send_skb()发送数据到用户态,或者(当auditd未启动时)直接printk到系统日志中;

5.当在audit_skb_queue中移除节点失败时,将创建一个等待实例,将其添加到kauditd_wait等待队列中;如果此时audit_skb_queue中确实没有节点了,那么将当前内核线程状态置为TASK_INTERRUPTIBLE,并且主动发起重新调度;否则,重新将当前进程状态恢复为TASK_RUNNING,并从等待队列中将其移除;

如果kauditd由于缓冲区链表中无数据而阻塞了,那么一旦审计系统又产生了新的套接字缓冲区时,它将会被唤醒。具体的唤醒动作在audit_log_end()中。

3.总结

审计内核模块和kauditd内核线程基本体现了审计在内核中的实现框架,前者负责处理用户态发送的命令请求,完成审计规则的操作任务;后者完成日志向用户态的合理传送。对于审计系统来说,用户更关心的是日志的产生。那么,审计日志是在内核哪个部分产生的?什么时机产生?这将涉及到审计系统中文件监控和进程监控的实现,请参考本系列下一篇文章。

参考资料:

1.Linux Audit :http://people.redhat.com/sgrubb/audit/

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

open()在Linux内核的实现(6)-打开操作分析

2015年4月19日 由 edsionte 没有评论 »

1.基本说明

当link_path_walk()进行完毕之后,也就意味着文件路径的查找位于最后一个目录项中,这个目录项是否存在目前还不明确,因为还需要配合用户使用open()时所传递的参数,有可能是打开已存在文件,有可能是创建新文件。

2.函数分析

2.1.do_last()

在path_openat()中,经过link_path_walk()对open路径的查找,将进入do_last()对路径中最后一个目录项做处理。最后一个目录项可能是各种类型,比如“.”或者“..”,也可能是符号链接文件或者是“/”,在此将对普通目录项(LAST_NORM)所进行的处理进行说明。

对于open系统调用来说,flags参数通过取不同的值可以进行不同的操作,下面将针对open最常用的两种操作进行说明,即打开文件和创建文件。

2.1.1.打开文件

如果使用open系统调用的作用仅仅是打开文件,那么flags中必然不会存在O_CREAT标志,基本步骤如下:

1.通过walk_component()对最后一个目录项进行查找。walk_component()首先在rcu模式下对目录项进行查找,如果查找失败则进行ref模式下的目录项查找。如果两种方式均查找失败,则返回值小于0;如果当前目录项为符号链接文件,则返回1,该函数将直接返回到path_openat()中,进而通过follow_link()进行处理;如果查找成功,则跳至ok标号处;

2.完成最后一个目录项的查找工作,即将进行真正的打开操作。不过,在这之前还需要通过may_open()对当前inode的权限和标志位进行检查;

3.如果一切顺利,则通过nameidata_to_filp()执行打开操作;

4.返回file结构。

2.1.2.创建文件

如果创建一个文件,那么flags必然设置标志O_CREAT。基本步骤如下:

1.首先会检查最后一个目录项是否以“/”结尾,如果是,则直接错误返回。否则,继续进行;

2.通过lookup_hash()对最后一个目录项进行查找,这个查找函数将返回一个dentry。如果最后一个目录项确实存在,那么dentry->d_inode不为空,那么说明要创建的这个文件事先是存在的。此时先进行标志位的判断,比如如果用户设置了O_EXCL标志,那么此时就不必继续进行直接错误返回。如果标志检查一切成功,则进行步骤4;否则,dentry->d_inode将为空,继续进行;

3.当最后一个目录项inode为空时,那么需要通过vfs_create()调用具体文件系统的create钩子函数创建这个文件;

4.通过may_open()检查inode标志位和权限的合法性;

5.通过nameidate_to_flip()调用具体文件系统的open钩子函数进行文件的打开工作;

6.返回file。

3.总结

通过前面的系列文章,说明了open()在Linux内核实现。事实上,这仅仅反映的是它在在虚拟文件系统的实现过程,主要涉及的内容为对路径的查找。最终,文件的打开操作必须经过具体文件系统的实现,虽然每个文件系统具体实现不同,但是他们的输入输出都将受到虚拟文件系统的限制保持一致。

参考资料:

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内核的实现(5)-符号链接目录项的处理

2015年3月20日 由 edsionte 没有评论 »

1.基本说明

在open()的路径查找过程中,walk_component()的作用就是对具体的目录项进行处理。它通过handle_dots()处理LAST_DOT和LAST_DOTDOT类型的目录项,通过do_lookup()处理非LAST_DOT和LAST_DOTDOT类型的目录项。当经过了do_lookup的处理后,还需要通过should_follow_link()检查当前的目录项是否是符号链接。

如果当前的目录项具备符号链接特性,那么内核将试图通过unlazy_walk()将当前的rcu-walk切换到ref-walk。因为处理处理符号链接文件会使用实体文件系统的钩子函数follow_link(),这个函数可能会引起阻塞,因此必须切换到允许阻塞的ref-walk模式。如果切换失败,则返回-ECHILD,即返回值do_filp_open()处,将重新以ref-walk方式执行打开操作;否则,walk_component()将返回1,那么调用它的link_path_walk()将通过nested_symlink()处理符号链接文件。

2.函数分析

2.1.nested_symlink()

该函数声明如下:
static inline int nested_symlink(struct path *path, struct nameidata *nd);

符号链接的处理过程并不复杂,但需要清楚link_count和total_link_count两个关键的变量的含义。这两个变量均来自当前进程描述符current中,前者表示当前符号链接连续嵌套的次数,后者表示一个路径中符号链接的总数。这两个计数均有最大限制,link_count最大不能超过MAX_NESTED_LINKS(默认为8),total_link_count最大不能超过40。

该函数的主要处理流程如下:

1.首先检查link_count是否超过了限制,如果超过限制则向上返回ELOOP错误;

2.如果一切正常,则nd和current同时更新嵌套计数器;

3.为了对符号链接文件嵌套情况的处理,接下来将进入循环处理过程。每次处理过程都首先通过follow_link()获取当前目录项所指文件,返回最后该文件路径的最后一个目录项;并且通过walk_component()更新nd;特殊情况下,当前目录项(即当前正在处理的符号链接文件的最后一个目录项)所指的文件仍然是个符号链接文件,因此walk_componet()将返回1,因此继续执行循环体,直至遇到非符号链接文件;

4.当本次的符号链接文件获取到最终的目标时,对nd和current的嵌套计数进行减一操作;

5.结束;

2.2.follow_link()

该函数声明如下:
static __always_inline int
follow_link(struct path *link, struct nameidata *nd, void **p);

该函数的作用是对这个符号链接文件所指的目标文件进行“一探究竟”,通过对目标文件进行查找操作进而更新nd。主要操作包含以下内容:

1.首先判断total_link_count是否超过了最大限制;

2.通过cond_resched()进行一次进程调度。通过walk_component()中的解释可知,只要某个目录项为符号链接文件,那么当前的walk模式肯定为ref,由于当前进程一开始处于rcu模式下,可能导致某些进程抢占失败,因此这里有必要进行一次进程调度;

3.对current进行total_link_count加一操作;

4.通过nd_set_link()对当前nd中saved_names[nd->depth]字段进行置空操作;

5.设置当前目录项的类型为LAST_BIND;

6.通过当前目录项中follow_link钩子函数进行符号链接文件的跟随操作,即将这个符号链接文件所指的目标文件路径返回至saved_names[nd->depth]中;

7.如果获取目标文件路径成功,则通过__vfs_follow_link()对这个目标文件路径进行全新的查找;

2.3.__vfs_follow_link()

该函数是对link_path_walk()的再次封装,其声明如下:
static int __vfs_follow_link(struct nameidata *nd, const char *link);

这种封装其实是合理的,因为既然对目标文件路径进行一个全新的查找工作,那可以使用前面的link_path_walk()函数,因为这个函数本身就是对路径的查找。

这里举个简单的例子(示例1)说明这种函数的调用情况。比如存在路径“/home/edsionte/files/abc/”,简称路径A,其中files目录是一个符号链接文件,其指向”/home/wang/test/exe/”目录(简称路径B,exe和test为普通目录)。如果要open路径A,那么简单的处理步骤是:

1.通过link_path_walk对路径A进行查找;

2.当walk_component()进行到files目录项时,发现其为符号链接文件,那么将通过nested_symlink()对这个目录项进行处理;

3.nested_symlink()中的follow_link()发现files指向的文件路径为路径B。为了检查路径B的合法性以及更新nd等信息,将通过__vfs_follow_link()对路径B进行查找工作;

4.调用link_path_walk()对路径B进行遍历,最终获取路径B的最后一个目录项exe;

5.nested_symlink()将执行walk_component(),并确定路径B最后的目录项test不是符号连接文件,因此退出nested_symlink();返回路径A的link_path_walk();

6.关于路径A的查找工作完毕;

特殊情况下(示例2),如果上述示例1中的路径B中,test目录同样为符号链接文件,并且指向名为C的路径“/home/tian/bin/”,其中,bin为普通目录;那么open路径A这个操作将会多一轮的处理流程。基于上述的处理流程步骤如下:

1.通过link_path_walk对路径A进行查找;

2.当walk_component()进行到files目录项时,发现其为符号链接文件,那么将通过nested_symlink()对这个目录项进行处理;

3.follow_link()发现files指向的文件路径为路径B。为了检查路径B的合法性以及更新nd等信息,将通过__vfs_follow_link()对路径B进行查找工作;

4.调用link_path_walk()对路径B进行遍历,当walk_component()进行至B中的test目录项时,由于其为符号链接文件,因此将调用nested_symlink();

5.nested_symlink()处理目录项test,由于test指向的路径C中所有目录项都是非符号链接文件,因此循环体执行一次,最终返回C的最后一个目录项bin至路径B的link_path_walk()中;

6.路径B的link_path_walk()继续进行,直至遍历到最后一个目录项exe;它将返回到3中的follow_link()中,接着执行walk_component()发现exe为普通目录项,则nested_symlink()结束;

7.路径A的link_path_walk()继续对路径A的下一个目录项abc进行walk_component();

通过上述示例可以看出,符号链接文件的嵌套数并不是指link_path_walk()的嵌套调用数目,而是指nested_symlink()的调用数。只有当符号链接文件(路径A)所指的文件(路径B)中依旧存在符号链接(指向路径C)时,才会出现nested_symlink()的嵌套。

3.总结

符号链接文件的处理方式比较特殊,但是本质的实现还是基于link_path_walk(),该函数是整个路径查找框架中的基础实现。对于一个路径来说,经过link_path_walk()的处理,nd将保存最后一个目录项的信息,但是最后一个目录项代表的文件(或目录)是否存在并不知晓,需要通过调用link_path_walk()的path_openat()做最后的处理。

参考资料:

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内核的实现(4)-普通目录项的处理

2015年3月3日 由 edsionte 没有评论 »

1.基本说明

在open()的路径查找过程中,walk_component()将对路径中的每个目录项进行遍历,当目录项为普通目录项时,将通过do_lookup()对其进行查找。在路径查找过程中,普通目录项算是比较常见的,因此更应该采取高效的方法节省查找时间。

为了提高查找效率,内核通过rcu-walk方式避免ref-walk由于锁机制而引入的阻塞问题;其次,内核通过dentry缓存的方式将目录项以哈希表的方式组织起来,以便提高查找效率。这两种方式在一定程度上可以提高路径查找的效率,不过它们并不是适合所有情况。比如,rcu-walk方式不能使用可能会引起阻塞的函数,有时候要查找的目录项并不位于缓存当中而只能从磁盘上获取信息。

由于上述提高查找效率的方式可能会查找失败,因此内核一般会经过多次目录项的查找。也就是说,内核会先进行高效的查找方式,如果在这种模式下查找失败,那么再进行传统的查找方式,保证查找工作的正常进行。具体的,内核会先进行rcu-walk方式,如果查找失败,则进行ref-walk方式;内核会优先在目录项缓存中进行快速的查找,如果查找失败,则通过访问磁盘进行慢速的查找。

2.函数实现

2.1.do_lookup()

内核通过do_lookup()进行普通目录项的查找工作。该函数声明如下:

tatic int do_lookup(struct nameidata *nd, struct qstr *name,
	struct path *path, struct inode **inode);

这个函数主要通过区分不同的情景来选择对应的查找方式。总体来说,先以rcu-walk方式在目录项缓存中查找,如果成功,则查找结束返回;否则,将rcu-walk切换到ref-walk模式;ref-walk模式下会先进行目录项缓存的查找,如果查找成功,则返回;否则,进行ref-walk下的磁盘查找;如果成功,则返回,否则,查找失败并返回错误码ENOENT;该错误码对应到用户态即为“No such file or directory”。

如果以rcu-walk模式进入该函数,则主要的查找步骤如下:

1.如果当前是rcu-walk模式(设置了LOOKUP_RCU),则先通过__d_lookup_rcu在目录项缓存中查找;

2.如果查找失败,则跳入unlazy标号处;如果查找成功,则继续;

3.通过follow_mount_rcu()判断当前的目录项是否为挂载点,如果是,则跨越挂载点;否则继续;

4.如果rcu-walk方式下查找缓存成功,则返回0;否则,进入unlazy标号;

5.unlazy标号是将当前的rcu-walk切换成ref-walk模式;如果切换成功,则继续;否则,返回ECHILD。即返回到do_filp_open()处,重新进行ref模式的查找;

6.当前查找模式为ref-walk,并且此时在缓存中并未找到对应的目录项;那么接下来必须试图在磁盘上进行查找了。不过在进行磁盘查找之前,还是会再次调用d_lookup()进行一次内存查找,因为retry标号下的代码有互斥锁,很可能该函数再此处会阻塞,而在阻塞阶段就目标目录项就有可能被载入内存,这样就可以省去在磁盘上查找目录项的工作;

7.如果d_lookup()查找成功,则继续;否则,通过d_alloc_and_lookup()分配并在磁盘上查找dentry;具体的,调用当前文件系统的lookup钩子函数;如果磁盘查找失败,则错误返回;否则,继续;

8.此时,已获取到要查找的目录项;则再通过follow_managed()对当前目录项进行检查,比如,检查当前目录项是否为挂载点;

9.获取当前目录项对应的inode信息;

10.返回;

如果以ref-walk模式进入该函数,则首先会通过__d_lookup()在目录项缓存中查找;如果查找成功,则接下来的步骤与rcu-walk步骤8-10相同;如果查找失败,则接下来的步骤与rcu-walk步骤6-10相同。

3.总结

普通目录项的查找工作其实并不复杂,无非是在内存中查找或在磁盘中查找。只不过,内核为了提高路径查找效率同时引入了rcu-walk方式,从而增加了代码分析的复杂度。如果当前目录项为符号链接文件,则内核的处理方式又是另一种方式,具体的实现过程可参见本系列的下一篇文章。

参考资料:

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内核的实现(3)-“.”和“..”的处理

2015年2月17日 由 edsionte 没有评论 »

1.基本说明

open()在内核的实现过程中,有一大部分工作都是路径查找。路径查找即对用户态传入的文件路径以目录项为单位进行依次遍历。目录项包含五种类型,当目录项为”.”(LAST_DOT)或者“..”(LAST_DOTDOT),那么walk_component()将通过handle_dots()对其进行处理。

实际上,如果当前目录项为“.”,那么该函数什么也不做直接返回(返回到link_path_walk中)即可。也就是说,如果当前目录项为”.”,那么walk_component()此时的作用就是“越过”这些当前目录,而nd信息不做改变,因为所有“.”之前的普通目录项已经更新了nd。比如/home/edsionte/./././doc,目录项edsionte之后的“.”对应的nd与edsionte目录项相同,因此walk_component()在遇到“.”时直接越过他们,进而处理doc目录项。

如果当前目录项为“..”,即当前要walk的目录项为上一次已经walk的目录项的父目录,也就是需要向上获取当前目录的父目录。

2.函数实现

2.1.handle_dots()

该函数的实现比较简单,内部根据当前的walk类型做了函数分发。

static inline int handle_dots(struct nameidata *nd, int type);

如果当前搜索路径的模式位rcu,则进入follow_dotdot_rcu()的流程;否则进入follow_dotdot()的流程。

2.2.follow_dotdot_rcu()

该函数的声明如下:

static int follow_dotdot_rcu(struct nameidata *nd);

该函数是在rcu模式下获取父目录项信息,如果搜索成功,则返回0;否则,返回ECHILD,也就是说需要切换到ref-walk方式下进行搜索路径。该函数的主要处理过程如下:

1.如果有需要的话,首先通过set_root_rcu()设置当前路径的根目录信息。可以在path_init()中获知,只有搜索路径是绝对路径,nd中的root才会在一开始就被设置;否则,比如是相对路径,那么这里就必须对根目录进行设置了。因为此处是向上搜索,可能会一直找到根目录处。

2.进入循环体,向上获取当前目录项的父目录项。通过情况下,这个循环体只会被执行一次即退出,只有当父目录项为一个挂载点时才有可能不断进行循环。

3.如果退出循环体,至此已经获取到了当前目录项的上一级目录项(即“..”所代表的父目录项)。

4.如果这个父目录项是一个挂载点,那么还需做一些特殊检查。因为在特殊情况下,当前这个父目录项又被挂载了其他的文件系统,那么返回上级目录这个操作获取的应该是最新文件系统的内容而不是之前那个文件系统的内容。通过__lookup_mnt()检查父目录下挂载的文件系统是否为最新的文件系统,如果是则检查结束;否则,将继续检查;

5.更新nd中的inode,并返回;

在循环体中,可能会出现三种情况:

a.如果当前目录项恰好为根目录目录项,则直接跳出循环;比如在根目录下执行“cd ../../../”;

b.如果当前目录项既不是根目录,也不是一个挂载点,则属于最普通的情况,即直接获取当前目录项的父目录项。方法很简单,直接用当前目录项的parent(nd->path.dentry->d_parent)覆盖当前的nd结构中的path.dentry即可;

c.如果当前目录项代表的是当前文件系统的根目录,则不能通过简单的获取当前目录项parent这种方法,因为获取的parent为“/”(当然指的是当前文件系统的根目录)。此时需要通过follow_up_rcu()对nd->path进行填充。如果follow_up_rcu()返回1,则循环继续;返回0,则结束循环;

2.3.follow_up_rcu()

如果“..”所代表的目录项正好是一个挂载点时,那么需要将当前的遍历从当前的文件系统向上(follow up)切换到父文件系统。该函数的声明如下:

static int follow_up_rcu(struct path *path);

既然是向上跨越到父文件系统,那么首先将父文件系统的挂载点对应的dentry(nd->path->mnt->mnt_mountpoint)赋值给当前nd中对应的dentry(nd->path->dentry)。因为这个挂载点也就是“..”对应的目录项,挂载点本质也就是个目录。

其次,将父文件系统的vfsmount结构(nd->path->mnt->mnt_parent)复制给当前nd中对应的mnt项(nd->path->mnt)。

通过以上两个步骤,即将当前的walk位置向上移动到父文件系统中的一个目录上。

下面通过举例说明上述过程。假设存在路径/home/edsionte/work,其中fs1文件系统下有目录/home/edsionte/work,文件系统fs2挂载在work目录下,即fs2的挂载点为work。假设work下有文件w1和file,在/home/edsionte/work/file目录下,用户访问的路径为“../w1”,则“..”对应的dentry即为fs2文件系统根目录,则需要跨越到fs1文件系统中。“跨越”操作首先是将当前dentry指向work,也就是下次将要在work目录下寻找w1文件;其次,需要改变当前文件系统状态,即通过替换vfsmount来体现。

完成以上操作,将返回1。也就是说,必须再进行一次follow_dotdot_rcu()中的循环过程,因为follow_up_rcu()完成的只是文件系统的跨越,跨越完毕后必须进行follow_dotdot_rcu()中的循环体工作。

更特殊的是,跨越文件系统之后的这个目录项很可能又是一个挂载点,那么又必须进入follow_up_rcu()中执行上述操作。只有当当前文件系统的挂载点就是自己的时候,即跨越到根文件系统的时候,该函数返回0,那么返回到上级函数follow_dotdot_rcu()中的循环体时,也将结束整个循环过程。

下面再通过举例说明follow_dotdot_rcu()中循环体反复执行的例子。假设fs1文件系统存在路径/home/edsionte/work,首先将fs2文件系统挂载于work下,再将fs3挂载在work下,那么此时fs3对应的父vfsmount为fs2对应的结构;再将fs4文件系统挂载在wrok下,那么fs4指向fs3。此刻,/home/edsionte/work可以访问fs4的内容,而其他之前挂载在这里的文件系统将被隐藏。假设用户在wrok目录下执行“cd ../”,用户想得到的结果是fs1文件系统下edsionte/下的内容。而此刻work位于fs4中,那么他必须向上逐步跨越文件系统,即fs4通过follow_up_rcu()跨越到父文件系统fs3,fs3再跨越到fs2,fs2再跨越到fs1。

2.4.follow_dotdot()

follow_dotdot()和follow_dotdot_rcu()的实现方式几乎一致,只不过该函数在内部实现上使用了读写锁。具体的,主要体现在follow_up()和follow_mount()两个函数的内部,它们的实现过程中均使用了读写锁,这在rcu-walk中是不允许的。

3.总结

本文针对open()在内核中的路径查找过程进行简单说明,并集中关注了“..”这个特殊的目录项。LAST_DOTDOT类型的目录项之所以特殊是由于它向上(follow up)的查找过程与当前所位于的路径查找过程(向下,follow down)相反,同时还会涉及到文件系统挂载点。如果您想了解其他类型目录项的查找过程,可以阅读本系列其他文章。

参考资料:

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

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