好贷网好贷款

内核态和用户态通讯知识收集

发布时间:2016-12-5 20:44:41 编辑:www.fx114.net 分享查询网我要评论
本篇文章主要介绍了"内核态和用户态通讯知识收集",主要涉及到内核态和用户态通讯知识收集方面的内容,对于内核态和用户态通讯知识收集感兴趣的同学可以参考一下。

内核态和用户态通讯知识收集 在一台运行   Linux   的计算机中,CPU   在任何时候只会有如下四种状态: 【1】   在处理一个硬中断。 【2】   在处理一个软中断,如   softirq、tasklet   和   bh。 【3】   运行于内核态,但有进程上下文,即与一个进程相关。 【4】   运行一个用户态进程。   1.  Linux中的进程间通信机制源自于Unix平台上的进程通信机制。Unix的两大分支AT&T Unix和BSD Unix在进程通信实现机制上的各有所不同,前者形成了运行在单个计算机上的System V IPC,后者则实现了基于socket的进程间通信机制。同时Linux也遵循IEEE制定的Posix IPC标准,在三者的基础之上实现了以下几种主要的IPC机制:管道(Pipe)及命名管道(Named Pipe),信号(Signal),消息队列(Message queue),共享内存(Shared Memory),信号量(Semaphore),套接字(Socket)。通过这些IPC机制,用户空间进程之间可以完成互相通信。为了完成内核空间与用户空间通信,Linux提供了基于socket的Netlink通信机制,可以实现内核与用户空间数据的及时交换。     2. 到目前Linux提供了9种机制完成内核与用户空间的数据交换,分别是内核启动参数、模块参数与 sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs和relayfs,其中模块参数与sysfs、procfs、debugfs、relayfs是基于文件系统的通信机制,用于内核空间向用户控件输出信息;sysctl、系统调用是由用户空间发起的通信机制。由此可见,以上均为单工通信机制,在内核空间与用户空间的双向互动数据交换上略显不足。Netlink是基于socket的通信机制,由于socket本身的双共性、突发性、不阻塞特点,因此能够很好的满足内核与用户空间小量数据的及时交互,因此在Linux 2.6内核中广泛使用,例如SELinux,Linux系统的防火墙分为内核态的netfilter和用户态的iptables,netfilter与iptables的数据交换就是通过Netlink机制完成。       3 Netlink机制及其关键技术    3.1 Netlink机制 Linux操作系统中当CPU处于内核状态时,可以分为有用户上下文的状态和执行硬件、软件中断两种。其中当处于有用户上下文时,由于内核态和用户态的内存映射机制不同,不可直接将本地变量传给用户态的内存区;处于硬件、软件中断时,无法直接向用户内存区传递数据,代码执行不可中断。针对传统的进程间通信机制,他们均无法直接在内核态和用户态之间使用,原因如下表: 通信方法  无法介于内核态与用户态的原因   管道(不包括命名管道)  局限于父子进程间的通信。   消息队列  在硬、软中断中无法无阻塞地接收数据。   信号量  无法介于内核态和用户态使用。   内存共享  需要信号量辅助,而信号量又无法使用。   套接字  在硬、软中断中无法无阻塞地接收数据。 4. 解决内核态和用户态通信机制可分为两类: 处于有用户上下文时,可以使用Linux提供的copy_from_user()和copy_to_user()函数完成,但由于这两个函数可能阻塞,因此不能在硬件、软件的中断过程中使用。 处于硬、软件中断时。  2.1   可以通过Linux内核提供的spinlock自旋锁实现内核线程与中断过程的同步,由于内核线程运行在有上下文的进程中,因此可以在内核线程中使用套接字或消息队列来取得用户空间的数据,然后再将数据通过临界区传递给中断过程. 2.2   通过Netlink机制实现。Netlink 套接字的通信依据是一个对应于进程的标识,一般定为该进程的 ID。Netlink通信最大的特点是对对中断过程的支持,它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,而是通过另一个软中断调用用户事先指定的接收函数。通过软中断而不是自行启动内核线程保证了数据传输的及时性 5.本人也写了这样一个例程,可以动态的将内核空间的物理地址和大小传给用户空间。本文也演示了内核空间和用户空间进行通信可以使用的两种常用方法:proc文件系统和mmap共享内存,整个内核模块,在模块插入时建立proc文件,分配内存。卸载模块的时候将用户空间写入的内容打印出来。 以下是内核模块的代码和用户空间的测试代码。 *This program is used to allocate memory in kernel and pass the physical address to userspace through proc file.*/ #include <linux/version.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/mm.h> #define PROC_MEMSHARE_DIR "memshare" #define PROC_MEMSHARE_PHYADDR "phymem_addr" #define PROC_MEMSHARE_SIZE "phymem_size" /*alloc one page. 4096 bytes*/ #define PAGE_ORDER 0 /*this value can get from PAGE_ORDER*/ #define PAGES_NUMBER 1 struct proc_dir_entry *proc_memshare_dir ; unsigned long kernel_memaddr = 0; unsigned long kernel_memsize= 0; static int proc_read_phymem_addr(char *page, char **start, off_t off, int count) {         return sprintf(page, "%08lx\n", __pa(kernel_memaddr)); } static int proc_read_phymem_size(char *page, char **start, off_t off, int count) {         return sprintf(page, "%lu\n", kernel_memsize); } static int __init init(void) {         /*build proc dir "memshare"and two proc files: phymem_addr, phymem_size in the dir*/         proc_memshare_dir = proc_mkdir(PROC_MEMSHARE_DIR, NULL);         create_proc_info_entry(PROC_MEMSHARE_PHYADDR, 0, proc_memshare_dir, proc_read_phymem_addr);         create_proc_info_entry(PROC_MEMSHARE_SIZE, 0, proc_memshare_dir, proc_read_phymem_size);         /*alloc one page*/         kernel_memaddr =__get_free_pages(GFP_KERNEL, PAGE_ORDER);         if(!kernel_memaddr)         {                 printk("Allocate memory failure!\n");         }         else         {                 SetPageReserved(virt_to_page(kernel_memaddr));                 kernel_memsize = PAGES_NUMBER * PAGE_SIZE;                 printk("Allocate memory success!. The phy mem addr=%08lx, size=%lu\n", __pa(kernel_memaddr), kernel_memsize);         }         return 0; } static void __exit fini(void) {         printk("The content written by user is: %s\n", (unsigned char *) kernel_memaddr);         ClearPageReserved(virt_to_page(kernel_memaddr));         free_pages(kernel_memaddr, PAGE_ORDER);         remove_proc_entry(PROC_MEMSHARE_PHYADDR, proc_memshare_dir);         remove_proc_entry(PROC_MEMSHARE_SIZE, proc_memshare_dir);         remove_proc_entry(PROC_MEMSHARE_DIR, NULL);         return; } module_init(init); module_exit(fini); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Godbach ([email][email protected][/email])"); MODULE_DESCRIPTION("Kernel memory share module.");   用户空间的测试代码: #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/mman.h> int main(int argc, char* argv[]) {         if(argc != 2)         {                 printf("Usage: %s string\n", argv[0]);                 return 0;         }                  unsigned long phymem_addr, phymem_size;         char *map_addr;         char s[256];         int fd;                  /*get the physical address of allocated memory in kernel*/         fd = open("/proc/memshare/phymem_addr", O_RDONLY);         if(fd < 0)         {                 printf("cannot open file /proc/memshare/phymem_addr\n");                 return 0;         }         read(fd, s, sizeof(s));         sscanf(s, "%lx", &phymem_addr);         close(fd);         /*get the size of allocated memory in kernel*/         fd = open("/proc/memshare/phymem_size", O_RDONLY);         if(fd < 0)         {                 printf("cannot open file /proc/memshare/phymem_size\n");                 return 0;         }         read(fd, s, sizeof(s));         sscanf(s, "%lu", &phymem_size);         close(fd);                  printf("phymem_addr=%lx, phymem_size=%lu\n", phymem_addr, phymem_size);         /*memory map*/         int map_fd = open("/dev/mem", O_RDWR);         if(map_fd < 0)         {                 printf("cannot open file /dev/mem\n");                 return 0;         }                  map_addr = mmap(0, phymem_size, PROT_READ|PROT_WRITE, MAP_SHARED, map_fd, phymem_addr);         strcpy(map_addr, argv[1]);         munmap(map_addr, phymem_size);         close(map_fd);         return 0;          }   测试的内核是2.6.25.以下是执行结果。 debian:/home/km/memshare# insmod memshare_kernel.ko debian:/home/km/memshare# ./memshare_user 'hello,world!' phymem_addr=e64e000, phymem_size=4096 debian:/home/km/memshare# cat /proc/memshare/phymem_addr  0e64e000 debian:/home/km/memshare# cat /proc/memshare/phymem_size  4096 debian:/home/km/memshare# rmmod memshare_kernel debian:/home/km/memshare# tail /var/log/messages Sep 27 18:14:24 debian kernel: [50527.567931] Allocate memory success!. The phy mem addr=0e64e000, size=4096 Sep 27 18:15:31 debian kernel: [50592.570986] The content written by user is: hello,world!   6. linux内核空间和用户空间通信   作者:harvey wang  邮箱:[email protected]   新浪博客地址:http://blog.sina.com.cn/harveyperfect,有关于减肥和学习英语相关的博文,欢迎交流   因网上已有很多介绍各种通信方式的示例代码,所以在本文中只是给出各种内核空间和用户空间通信方式的介绍说明。希望给像我一样的初学者提供一定的指导。因水平有限,欢迎各位批评指点。   1         概述 Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间“)。除了进程之间的通信外,在嵌入式设计中还经常需要进行内核空间和用户空间的信息交互。本文主要讨论内核空间和用户空间信息交互的方法。 1.1 处理器状态 处理器总处于以下状态中的一种: A、内核态,运行于进程上下文,内核代表进程运行于内核空间; B、内核态,运行于中断上下文,包括硬中断和软中断; C、用户态,运行于用户空间。   1.2 不同状态的限制 根据上面的状态分类,内核空间和用户空间之间的信息交互就分为两类,即中断上下文内核态空间与进程空间信息交互;进程上下文内核态空间和进程空间信息交互。   内核态环境 进入内核态的方式 局限性 说明 进程上下文 在进程中通过系统调用进入内核态,内核态代码与该进程相关。 内核空间和进程空间的虚拟地址不同,不能直接传递信息。 该进程的页表基地址依然在页表基地址寄存器(如X86中的CR3)中,内核空间中可以使用__user 强制使用用户空间的地址,从而进行数据交互。 中断上下文 硬件触发中断,或内核中挂接软中断。不与特定的进程相关。 内核空间和进程空间的虚拟地址不同,不能直接传递信息。 中断中不能睡眠,不能运行引起阻塞的函数。 由于中断触发的随机性,中断上下文内核态不与特定的进程相关。       2 各种通信方式 本节说明各种通信方式是否适合内核空间和用户空间信息交互,以及如何使用。   2.1 信号 在进程中使用函数signal()或sigaction()安装信号时指定了关联的函数。在内核空间相进程发送信号,从内核空间返回进程空间时检查并执行相应的关联函数。 在进程中可以使用pause()函数进入睡眠,在有信号产生并执行了相应的关联函数后进程被唤醒,继续执行。可以使用这种方式实现内核空间和用户空间的同步。 pause()会使当前进程挂起,直到捕捉到一个信号,对指定为忽略的信号,pause()不会返回。只有执行了一个信号处理函数,并从其返回,puase()才返回-1,并将errno设为EINTR。          2.2 信号量        虽然原理一样,但内核空间和用户空间的信号量是完全两套系统,所以信号量不能用于内核空间和用户空间信息交互。   2.3 无名管道        无名管道只适用于有关系的进程之间通信。不能用于内核空间和用户空间信息交互。   2.4 get_user()/put_user() get_user(x, ptr):本函数是在内核中被调用,获取用户空间指定地址的数值(一个字节或字)并保存到内核变量x中,ptr为用户空间的地址。用法举例如下:get_user(val, (int __user *)arg) put_user(x, ptr):在内核中被调用,将内核空间的变量x的数值(一个字节或字)保存到用户空间指定地址处,prt为用户空间地址。用法举例如下:put_user(val, (int __user *)arg) 注明:函数用于进程上下文内核态空间,即通常在系统调用函数中使用该函数,如设备驱动中ioctl函数中。     2.5 copy_from_user()/copy_to_user() 主要应用于设备驱动中读写函数中,通过系统调用触发,在当前进程上下文内核态运行(即当前进程通过系统调用触发)。 unsigned long copy_to_user(void __user *to, const void *from, unsigned long n) 通常用在设备读函数或ioctl中获取参数的函数中:其中“to”是用户空间的buffer地址,在本函数中将内核buffer“from”除的n个字节拷贝到用户空间的“to”buffer。   unsigned long copy_from_user(void *to, const void __user *from, unsigned long n) 通常用在设备写函数或ioctl中设置参数的函数中:“to”是内核空间的buffer指针,要写入的buffer;“from”是用户空间的指针,数据源buffer。 注意:中断代码时不能用这两个函数,因为其调用了might_sleep()函数,会导致睡眠,并且这两个函数要求在进程上下文内核态空间中运行。   这两个函数不能直接在中断中使用,但是可以变通一下,在中断中向进程发送信号通知进程有数据准备好。在进程执行时调用read函数,在read函数中调用copy_to_user函数,从而实现中断触发,把数据从内核空间拷贝到用户空间的需要。   2.6 共享内存(mmap)        使用mmap()函数通常映射一个普通文件实现进程之间内存共享,即多个进程打开同一个文件,将文件映射到各自进程的虚拟空间。这样各个进程就可以通过共享的内存进行大量的数据交互,当然需要我们自己设计互斥功能。        还可以使用mmap()函数实现内核空间和用户空间内存共享的功能。网上提到的方法基本都是proc文件+mmap。 大体过程如下 1、在模块中申请一些内存页面,作为共享的内存空间。 2、创建可读的proc文件,在其读函数中把上面申请的内存空间的物理地址返回给进程空间。 3、在进程空间open /dev/mem文件,并把从proc读取的物理地址(要共享的内存的物理地址)作为文件/dev/mem的offset,以此offset 把/dev/mem文件的若干空间用mmap映射到进程空间。 注意: 1、/dev/mem 不是一个普通的文件里面的内容是所有物理内存的内容信息。所以,在上面的过程中把共享空间的物理地址作为offset使用。 2、proc文件的作用就是提供一个读取的函数,把共享内存的地址从内核空间传递到用户空间。也可以用设备的ioctl 把该物理地址数值传给用户空间。  7. 为了更好的理解和描述通信原理,需要先认识一下4个重要的数据结构,分别是file_operations,file,inode,dentry。   文件操作集 file_operations结构包含了一组函数指针,每个打开的文件(在内核里面由file结构表示)和一组函数关联(通过file结构中指向file_operations结构的f_op字段)。这些操作组要用来实现系统调用,例如open,read等等。   file结构 file结构表示一个打开的文件(系统中每个打开的文件在内核空间都对应一个file结构)。它由内核在open时创建,由close释放(进程撤销时也会释放)。 file结构中最重要的成员罗列如下: loff_t f_pos; 当前读/写位置 struct file_operations *f_op; 指向一个文件操作集结构。内核在执行open时对这个指针赋值,以后要处理相关系统调用时就直接调用这个指针。 例如:write()系统调用时将直接调用file->f_op->write()。 struct dentry *f_dentry;        文件对应的目录项结构。可以通过file->f_dentry->d->d_inode来访问索引节点。   inode结构 内核用inode结构在内部表示文件,它和file结构不同,后者表示打开的文件描述符,对于一个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向唯一的inode对象。 inode结构中最重要的成员罗列如下: dev_t i_rdev;        对表示设备文件的inode结构,该字段包含了设备标号(主/次设备号)。 struct cdev *i_cdev;        指向一个字符设备驱动程序对象。 const struct inode_operations*i_op;       指向索引结点操作集。       例如,系统调用mkdir将会调用这里的函数inode->i_op->mkdir() const struct file_operations*i_fop;        指向文件操作集。当执行open时,将使用这个这里的指针赋值file结构的f_op。   dentry结构 对于进程查找的每个路径名的分量,内核都为其创建一个dentry结构。        例如,在查找路径名/dev/test时,内核为根目录“/”创建一个dentry结构,为dev创建二级dentry结构,为test创建三级dentry结构。   经典的内核与用户空间的通信(使用read和write系统调用) 1.      用户空间需要将某些数据传递给内核,并指定数据的处理函数。 2.      用户空间需要从内核读取数据,并指定数据的读取函数。   当执行open时,file结构的f_op将会指向要求的操作集函数集,也就是inode结构中的i_fop。通信的实现机制就是,注册inode结构,令inode的i_fop指向要求的操作集函数集,当系统打开这个文件时,使用read和write系统调用时,将会调用要求的操作函数。 用于通信的文件系统是一种特殊的文件系统,在系统初始化时一般只有一个根inode和根dentry结构,每个inode和dentry结构对象都对应一些内核数据和内核操作函数集。 例如:proc文件系统,初始化时只建立根路径/proc的inode和dentry结构,当第一次访问/proc下的某个文件时,将从根dentry结构开始查找并建立相应的inode和dentry结构,并将这些结构加入相应的缓存,当第二次访问时,可以直接从缓存中获得该结构。 这里就会涉及到4个关键的问题: inode和dentry结构是如何建立的?是根据什么数据建立的?inode和dentry结构对应的是哪些数据和哪些操作函数集?inode和dentry结构对应的数据和操作函数集在内核里是如何组织的?   下面将会围绕以上4个问题分析4种最重要的通信机制: 1.      misc设备(可读,可写,可进行内存映射) 2.      proc(可读,可写,一般不进行内存映射) 3.      sysctl(可读,可写) 4.      seq_file(只读)   (2)misc设备实现分析 每个misc设备在/dev下都有一个设备文件,/dev下的每个设备文件都对应一个inode对象,一个misc设备用struct miscdevice结构来表示,如下: struct miscdevice  {        int minor;次设备号        const char *name;设备名称        const struct file_operations *fops;设备操作函数集        struct list_head list;        struct device *parent;        struct device *this_device;        const char *nodename;        mode_t mode; }; 下面将分析以下几个过程: 1.      如何根据miscdevice结构在/dev下建立设备文件? 2.      miscdevice结构是如何组织的? 3.      如何根据/dev下的设备文件定位miscdevice结构?   将会涉及到的概念: 主设备号:通常是标识设备对应的设备驱动程序(也可以说是标识设备的类别)。 次设备号:在某个类里面确定一个具体的设备。 inode中与设备有关的成员变量如下: dev_t  i_rdev:设备编号,包括主设备号,次设备号。 struct cdev *  i_cdev:指向字符设备驱动程序,根据主设备号定位。 其中misc设备的主设备号是10。   例子:注册一个misc设备 static struct file_operations audio_fops = { 定义操作函数集        .owner          = THIS_MODULE,        .open            = audio_in_open,        .release  = audio_in_release,        .read             = audio_in_read,        .write            = audio_in_write,        .unlocked_ioctl   = audio_in_ioctl, }; struct miscdevice audio_in_misc = { misc设备对象        .minor    = MISC_DYNAMIC_MINOR,次设备号        .name     = "msm_pcm_in",设备名称        .fops      = &audio_fops,操作函数集 }; static int __init audio_in_init(void) {        misc_register(&audio_in_misc);向内核注册misc设备        return 1; } 这个例子会在/dev下生成一个设备文件,文件名是"msm_pcm_in",文件对应的inode->rdev的主设备号是10,次设备号是MISC_DYNAMIC_MINOR,inode关联的操作函数集是audio_fops。   在/dev下建立inode对象的函数调用流程如下: int misc_register(struct miscdevice * misc)à struct device *device_create()à struct device *device_create_vargs()à int device_register(struct device *dev)à int device_add(struct device *dev)à int devtmpfs_create_node(struct device *dev)à int vfs_mknod(struct inode *dir, struct dentry *dentry, int mode, dev_t dev)à error = dir->i_op->mknod(dir, dentry, mode, dev);à 不同的文件系统i_op->mknod()指向不同的函数。 例如,如果/dev是ext3文件系统,则调用: static int ext3_mknod ()à接着调用以下函数初始化inode对象 init_special_inode(inode, inode->i_mode, rdev);函数定义如下: void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev) {     inode->i_mode = mode;     if (S_ISCHR(mode)) {misc设备是一种特殊的字符设备       inode->i_fop = &def_chr_fops;       inode->i_rdev = rdev;设备号    } else if (S_ISBLK(mode)) {        inode->i_fop = &def_blk_fops;        inode->i_rdev = rdev;     } else if (S_ISFIFO(mode))        inode->i_fop = &def_fifo_fops;     else if (S_ISSOCK(mode))        inode->i_fop = &bad_sock_fops;     else        printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o) for"                 " inode %s:%lu\n", mode, inode->i_sb->s_id,                 inode->i_ino); } 经过这个过程建立的dentry结构和inode结构如图2-1所示,图中的i_rdev是用于定位某个struct miscdevice结构。 图 2-1 misc设备文件dentry结构和inode结构 其中const struct file_operations def_chr_fops = {     .open = chrdev_open, }; 现在inode对象建立完毕,这个inode对象只包含了两个信息: 1.   设备号,2.字符设备操作函数集def_chr_fops。 下面分析如何根据这两个信息定位miscdevice结构。 当用户程序执行open()打开/dev下的misc设备文件时,内核首先会调用inode->i_fop->open(),也就是chrdev_open(),如下: static int chrdev_open(struct inode *inode, struct file *filp) {        struct cdev *p;        struct cdev *new = NULL;        int ret = 0;        spin_lock(&cdev_lock);        p = inode->i_cdev;        if (!p) {如果字符设备驱动程序对象为空               struct kobject *kobj;               int idx;               spin_unlock(&cdev_lock);               kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);//根据i_rdev内的主设备号               if (!kobj)                             //查找并返回驱动程序对象的kobj对象。                      return -ENXIO;               new = container_of(kobj, struct cdev, kobj);//根据kobj对象地址获得设备驱动程序对象               spin_lock(&cdev_lock);               /* Check i_cdev again in case somebody beat us to it while                  we dropped the lock. */               p = inode->i_cdev;               if (!p) {                      inode->i_cdev = p = new; //关联设备驱动程序对象                      list_add(&inode->i_devices, &p->list);                      new = NULL;               } else if (!cdev_get(p))                      ret = -ENXIO;        } else if (!cdev_get(p))               ret = -ENXIO;        spin_unlock(&cdev_lock);        cdev_put(new);        if (ret)               return ret;        ret = -ENXIO;        filp->f_op = fops_get(p->ops); //获得设备驱动程序对象的操作函数        if (!filp->f_op)               goto out_cdev_put;        if (filp->f_op->open) {               ret = filp->f_op->open(inode,filp); //调用设备驱动程序的open()函数               if (ret)                      goto out_cdev_put;        }        return 0;  out_cdev_put:        cdev_put(p);        return ret; } 上边涉及到struct kobject结构,在/sys下显示的每个文件都对应一个kobject,一般每个kobject对象又嵌入到另一个对象中,例如: struct cdev {字符设备驱动程序对象        struct kobject kobj; 内嵌的kobject对象        struct module *owner;        const struct file_operations *ops;操作函数集        struct list_head list;        dev_t dev; 主设备号        unsigned int count; }; 知道了内嵌的kobject对象的地址,再做一个偏移也就得到了cdev对象的地址。 每个misc设备是主设备号为10的字符设备,内核在misc_init()中注册了 misc字符设备的驱动程序对象,如下: register_chrdev(MISC_MAJOR,"misc",&misc_fops) 其中主设备号 MISC_MAJOR = 10 驱动程序操作函数集:static const struct file_operations misc_fops = {        .owner          = THIS_MODULE,        .open            = misc_open, }; chrdev_open()函数功能解释: 1.      根据设备号i_rdev中的主设备号(10)找到kobject对象à 2.      根据kobject对象找到struct cdev对象(“misc”)à 3.      调用struct cdev对象(“misc”)的open()函数(也就是misc_open())。   static int misc_open(struct inode * inode, struct file * file) {        int minor = iminor(inode); 获得次设备号        struct miscdevice *c;        int err = -ENODEV;        const struct file_operations *old_fops, *new_fops = NULL;        mutex_lock(&misc_mtx);        list_for_each_entry(c, &misc_list, list) { 根据次设备号查找misc设备链表               if (c->minor == minor) {                      new_fops = fops_get(c->fops);找到miscdevice操作函数                              break;               }        }        if (!new_fops) {               mutex_unlock(&misc_mtx);               request_module("char-major-%d-%d", MISC_MAJOR, minor);               mutex_lock(&misc_mtx);                 list_for_each_entry(c, &misc_list, list) {                      if (c->minor == minor) {                             new_fops = fops_get(c->fops);                             break;                      }               }               if (!new_fops)                      goto fail;        }        err = 0;        old_fops = file->f_op;        file->f_op = new_fops;赋予新的操作函数        if (file->f_op->open) {               file->private_data = c;               err=file->f_op->open(inode,file);执行miscdevice的open函数               if (err) {                      fops_put(file->f_op);                      file->f_op = fops_get(old_fops);               }        }        fops_put(old_fops); fail:        mutex_unlock(&misc_mtx);        return err; } 执行misc_open()函数之后,file对象的f_op将指向miscdevice对象的fops。 上边还涉及到2个查找过程:(图2-2,图2-3展示了查找过程) 1.      如何根据主设备号(10)找到对应的字符设备驱动程序? 2.      如何根据次设备号找到miscdevice对象?   图2-2是字符设备驱动程序对象的组织结构,其中cdev_map是全局变量,每个probe都对应一个字符设备驱动程序对象,其中probe->data指向字符设备驱动程序对象,使用register_chrdev()就是向这里注册一个字符设备驱动程序对象,其它内核路径将会根据主设备号在这个表格内查找对应的字符设备驱动程序。   图 2-2 字符设备驱动程序对象组织结构   图2-3是miscdevice对象的组织结构,其中misc_list是全局变量,chrdev_open()将根据次设备号找到对应的miscdevice对象,也就找到了我们需要关联的操作函数集。   图 2-3 misc设备组织   (2.2)使用misc设备实现通信 1.      建立struct miscdevice结构,其中的成员变量fops指向我们自己定义的操作函数集。 2.      调用misc_register()注册miscdevice结构,将会在/dev下生成一个设备文件。 3.      使用open()打开这个设备文件,则file对象的f_op将会指向我们自己定义的操作函数集。 例如用户空间使用read()函数,将会定位到,miscdevice->fops->read()   使用系统调用read()和write()是一种经典的通信流程,它必须进行数据的拷贝,也就是说,用户空间想使用内核空间的数据,必须将这个数据拷贝到用户空间,而内核空间想引用用户空间的数据,也必须将数据拷贝到内核空间,效率是比较低的。   必须进行数据的拷贝原因有2点: 1.      用户空间使用的线性地址是在前3个G的空间,而内核使用的线性地址是在第4个G,所以用户空间没法使用内核中的线性地址。 2.      当前运行进程是不断替换的,内核是使用当前运行进程的页表,虽然不同进程的页表的内核空间部分是相同的,但是用户空间部分是不同的,如果内核要长期使用某个用户空间的数据,将会出现问题(同样的线性地址,在不同的进程上下文中将会引用不同的物理地址)。   共享内存通信方式 为了避免数据的拷贝,我们可以使用共享内存,也就是注册 miscdevice->fops->mmap()。 int (*mmap) (struct file *filp, struct vm_area_struct *vm);        实现步骤: 1. 用户空间执行mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)。内核最终会调用miscdevice->fops->mmap()。 2. 在调用这个函数之前,内核会做一些预备工作:根据用户空间提交的参数分配一块线性空间(前3个G的某个空间),分配的线性空间用结构struct vm_area_struct描述。 3. 最终在miscdevice->fops->mmap()内可以将某块物理内存映射到这个线性空间。 经过以上步骤,用户空间的某块线性空间和内核的某块线性空间将会指向相同的物理地址。 这种通信方式应该是最快的,因为没有数据的拷贝,也没有额外的函数调用,仅仅是内存的读取和写入。很大的缺点就是没有同步机制。   (3)  procfs实现分析 procfs是比较老的一种用户态与内核态的数据交换方式,内核的很多数据都是通过这种方式出口给用户的,内核的很多参数也是通过这种方式来让用户方便设置的。procfs提供的大部分内核参数是只读的。实际上,很多应用严重地依赖于procfs,因此它几乎是必不可少的组件。 procfs提供了如下API: struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,                                           struct proc_dir_entry *parent) 该函数用于创建一个正常的proc条目,参数name给出要建立的proc条目的名称,参数mode给出了建立的该proc条目的访问权限,参数parent指定建立的proc条目所在的目录。如果要在/proc下建立proc条目,parent应当为NULL。否则它应当为proc_mkdir返回的struct proc_dir_entry结构的指针。 struct proc_dir_entry *proc_mkdir(const char * name, struct proc_dir_entry *parent) 该函数用于创建一个proc目录,参数name指定要创建的proc目录的名称,参数parent为该proc目录所在的目录。   例子:创建一个proc下的文件 int exam_read_proc(char *page, char **start, off_t off, int count, int *eof, void *data) {读函数         count = sprintf(page, "%d", *(int *)data);         return count; } int exam_write_proc(struct file *file, const char __user *buffer, unsigned long count, void *data) {写函数        printk(“exam write\n”);         return 3; } static int __init procfs_exam_init(void) {               struct proc_dir_entry * entry;         entry = create_proc_entry("examproc", 0644, NULL);注册proc_dir_entry结构         if (entry) {                 entry->data = &string_var;                 entry->read_proc = &exam_read_proc;赋读函数地址                 entry->write_proc = &exam_write_proc;赋写函数地址         }         return 0; } static void __exit procfs_exam_exit(void) {         remove_proc_entry("examproc", NULL);撤销proc_dir_entry结构 } module_init(procfs_exam_init); module_exit(procfs_exam_exit); 这个模块会在/proc目录下创建一个文件,文件名是“examproc”,读文件时将执行exam_read_proc(),写文件时将执行exam_write_proc()。 注意到这里并没有涉及dentry对象和inode对象,下面将说明如何根据proc_dir_entry建立相应的dentry对象和inode对象。 proc下面的每个目录和文件都对应一个struct proc_dir_entry对象,proc_dir_entry对象组织结构如图3-1所示:   图3-1 proc_dir_entry对象组织结构 和/proc目录一样proc_dir_entry对象也组成树状结构,/proc下的每个目录和文件都对应一个proc_dir_entry对象。 在proc文件系统加载之初,只有根目录/proc已经生成对应的dentry对象和inode对象, 如图3-2所示,/proc下的其它目录和文件在第一被访问到时,才开始建立对应dentry对象和inode对象。        从图3-2中已经可以看出proc文件系统是如何根据proc_dir_entry建立起dentry对象和inode对象。图3-2的proc_dir_entry对应目录/proc。   图3-2 proc根目录对象 其中static const struct inode_operationsproc_dir_inode_operations = {        .lookup        = proc_lookup,        .getattr   = proc_getattr,        .setattr   = proc_notify_change, }; static const struct file_operations proc_dir_operations = {        .llseek                  = generic_file_llseek,        .read                    = generic_read_dir,        .readdir         = proc_readdir, };   当系统第一次查找/proc下的某个文件时,将以这个dentry对象作为入口点。 例如:要打开文件/proc/examproc, 1.      根据路径/proc得到proc根dentry对象,得到inode对象地址,偏移得到对应的proc_inode对象,接着得到对应的dir_proc_dentry对象。 2.      查找1中的dir_proc_dentry对象的子对象,查看是否存在名字是examproc的dir_proc_dentry对象。 3.      如果2中找到对应的dir_proc_dentry对象,将会根据这个对象建立dentry对象和inode对象。   图3-2所示的是非文件dentry对象,这里要建立的是一个文件dentry对象,结构如图3-3所示:   图3-3 proc文件目录对象结构 其中static const struct inode_operationsproc_file_inode_operations = {        .setattr   = proc_notify_change, }; static const struct file_operations proc_file_operations = {        .llseek           = proc_file_lseek,        .read            = proc_file_read,        .write           = proc_file_write, }; 图3-2和图3-3的不同点在于函数指针不同。 static ssize_t   proc_file_write(struct file *file, const char __user *buffer, size_t count, loff_t *ppos) {        struct proc_dir_entry *pde = PDE(file->f_path.dentry->d_inode);        ssize_t rv = -EIO;        if (pde->write_proc) {               spin_lock(&pde->pde_unload_lock);               if (!pde->proc_fops) {                      spin_unlock(&pde->pde_unload_lock);                      return rv;               }               pde->pde_users++;               spin_unlock(&pde->pde_unload_lock);                 /* FIXME: does this routine need ppos?  probably... */               rv = pde->write_proc(file, buffer, count, pde->data);               pde_users_dec(pde);        }        return rv; } 当执行write时将会执行proc_file_write(),接着将会调用pde->write_proc(file, buffer, count, pde->data); 执行read()函数是类似。   注意:当/proc下的某个文件第一次被访问过之后,系统会将相应的目录对象和inode加入相应的缓存,以后就从相应的缓存读取目录对象或者inode对象。   (4)sysctl文件系统实现分析 sysctl是一种用户应用来设置和获得运行时的内核的配置参数的一种有效方式,通过这种方式,用户应用可以在内核运行的任何时刻来改变内核的配置参数,也可以在任何时候获得内核的配置参数,通常,内核的这些配置参数也出现在proc文件系统的/proc/sys目录下,用户应用可以直接通过这个目录下的文件来实现内核配置的读写操作,例如,用户可以通过 cat /proc/sys/net/ipv4/ip_forward 来得知内核IP层是否允许转发IP包,用户可以通过 echo 1 > /proc/sys/net/ipv4/ip_forward 把内核 IP 层设置为允许转发 IP包,即把该机器配置成一个路由器或网关。 先从一个实际的例子讲起。 struct ctl_path net_ipv4_ctl_path[] = {        { .procname = "net", },        { .procname = "ipv4", },        { }, }; static struct ctl_table ipv4_net_table[] = {        {               .procname     = "icmp_echo_ignore_all",               .data             = &init_net.ipv4.sysctl_icmp_echo_ignore_all,               .maxlen         = sizeof(int),               .mode           = 0644,               .proc_handler      = proc_dointvec        },        { } }; __register_sysctl_paths(&net_sysctl_root,                                    &namespaces, net_ipv4_ctl_path, ipv4_net_table); 这个例子将会在/proc/sys/net/ipv4下建立一个文件icmp_echo_ignore_all,当对这个文件进行读写时,实际上是对整形变量init_net.ipv4.sysctl_icmp_echo_ignore_all进行读写。   下面看一下内核是如何实现的。 首先会生成如下的结构,并加入struct ctl_table_header root_table_header的中链表。   图4-1 sysctl目录树组织结构 当/proc/sys下的某个目录和文件被第一次访问时,就是根据这个结构进行查找和生成对应的dentry和inode对象。 sysctl是proc下的特殊的目录,它使用自己的dentry和inode对象生成函数。 /proc/sys下的根目录结构如图4-2所示:   图4-2 proc_sys根目录对象 其中static const struct inode_operationsproc_sys_dir_operations= {        .lookup        = proc_sys_lookup,  sys子目录搜索生成函数        .permission   = proc_sys_permission,        .setattr   = proc_sys_setattr,        .getattr   = proc_sys_getattr, }; 当第一次搜索/proc/sys某个文件时,将会以这个根目录为入口点。 首先根据路径/proc/sys生成对应的目录和inode对象,接着可以获得root_table_header,然后将会获得struct ctl_table_header链表,遍历这个链表,找到是否有匹配的ctl_table对象,并生成对应的目录和inode对象。/proc/sys下的文件对应的结构如图4-3所示:   图4-3 sysctl文件目录对象 其中static const struct file_operationsproc_sys_file_operations = {        .read             = proc_sys_read,        .write            = proc_sys_write, }; 下面分析一下proc_sys_write函数 static ssize_t proc_sys_write(struct file *filp, const char __user *buf,                             size_t count, loff_t *ppos) {        return proc_sys_call_handler(filp, (void __user *)buf, count, ppos, 1); }   static ssize_t proc_sys_call_handler(struct file *filp, void __user *buf,               size_t count, loff_t *ppos, int write) {        struct inode *inode = filp->f_path.dentry->d_inode;获得inode对象        struct ctl_table_header *head = grab_header(inode);获得控制结构        struct ctl_table *table = PROC_I(inode)->sysctl_entry;获得ctl_table对象        ssize_t error;        size_t res;        if (IS_ERR(head))               return PTR_ERR(head);        /*         * At this point we know that the sysctl was not unregistered         * and won't be until we finish.         */        error = -EPERM;        if (sysctl_perm(head->root, table, write ? MAY_WRITE : MAY_READ))               goto out;          /* if that can happen at all, it should be -EINVAL, not -EISDIR */        error = -EINVAL;        if (!table->proc_handler)               goto out;          /* careful: calling conventions are nasty here */        res = count;        error = table->proc_handler(table, write, buf, &res, ppos);        if (!error)               error = res; out:        sysctl_head_finish(head);        return error; } 从代码可以看出write系统调用在内核里面就是根据ctl_table结构操作相应的函数和数据。 read系统调用类似。   (5)seq_file 一般地,内核通过在procfs文件系统下建立文件来向用户空间提供输出信息,用户空间可以通过任何文本阅读应用查看该文件信息,但是procfs有一个缺陷,如果输出内容大于1个内存页,需要多次读,因此处理起来很难,另外,如果输出太大,速度比较慢,有时会出现一些意想不到的情况,Alexander Viro实现了一套新的功能,使得内核输出大文件信息更容易,该功能出现在2.4.15(包括2.4.15)以后的所有2.4内核以及2.6内核中,尤其是在2.6内核中,已经大量地使用了该功能。 要想使用seq_file功能,开发者需要包含头文件linux/seq_file.h,并定义与设置一个seq_operations结构(类似于file_operations结构): struct seq_operations {         void * (*start) (struct seq_file *m, loff_t *pos);         void (*stop) (struct seq_file *m, void *v);         void * (*next) (struct seq_file *m, void *v, loff_t *pos);         int (*show) (struct seq_file *m, void *v); }; start函数用于指定seq_file文件的读开始位置,返回实际读开始位置,如果指定的位置超过文件末尾,应当返回NULL,start函数可以有一个特殊的返回SEQ_START_TOKEN,它用于让show函数输出文件头,但这只能在pos为0时使用,next函数用于把seq_file文件的当前读位置移动到下一个读位置,返回实际的下一个读位置,如果已经到达文件末尾,返回NULL,stop函数用于在读完seq_file文件后调用,它类似于文件操作close,用于做一些必要的清理,如释放内存等,show函数用于格式化输出,如果成功返回0,否则返回出错码。 Seq_file也定义了一些辅助函数用于格式化输出: int seq_putc(struct seq_file *m, char c); 函数seq_putc用于把一个字符输出到seq_file文件。 int seq_puts(struct seq_file *m, const char *s); 函数seq_puts则用于把一个字符串输出到seq_file文件。 int seq_escape(struct seq_file *, const char *, const char *); 函数seq_escape类似于seq_puts,只是,它将把第一个字符串参数中出现的包含在第二个字符串参数中的字符按照八进制形式输出,也即对这些字符进行转义处理。 int seq_printf(struct seq_file *, const char *, ...)         __attribute__ ((format (printf,2,3))); 函数seq_printf是最常用的输出函数,它用于把给定参数按照给定的格式输出到seq_file文件。 int seq_path(struct seq_file *, struct vfsmount *, struct dentry *, char *); 函数seq_path则用于输出文件名,字符串参数提供需要转义的文件名字符,它主要供文件系统使用。 在定义了结构struct seq_operations之后,用户还需要把打开seq_file文件的open函数,以便该结构与对应于seq_file文件的struct file结构关联起来,例如,struct seq_operations定义为: struct seq_operations exam_seq_ops = {        .start = exam_seq_start,    .stop = exam_seq_stop,    .next = exam_seq_next,    .show = exam_seq_show }; 那么,open函数应该如下定义: static int exam_seq_open(struct inode *inode, struct file *file) {         return seq_open(file, &exam_seq_ops); }; 注意,函数seq_open是seq_file提供的函数,它用于把struct seq_operations结构与seq_file文件关联起来。最后,用户需要如下设置struct file_operations结构: struct file_operations exam_seq_file_ops = {         .owner   = THIS_MODULE,         .open    = exm_seq_open,         .read    = seq_read,         .llseek  = seq_lseek,         .release = seq_release }; 注意,用户仅需要设置open函数,其它的都是seq_file提供的函数。 然后,用户创建一个/proc文件并把它的文件操作设置为exam_seq_file_ops即可:     struct proc_dir_entry *entry; entry = create_proc_entry("exam_seq_file", 0, NULL); if (entry) entry->proc_fops = &exam_seq_file_ops; 对于简单的输出,seq_file用户并不需要定义和设置这么多函数与结构,它仅需定义一个show函数,然后使用single_open来定义open函数就可以,以下是使用这种简单形式的一般步骤:   1.定义一个show函数 int exam_show(struct seq_file *p, void *v) { … } 2. 定义open函数 int exam_single_open(struct inode *inode, struct file *file) {         return(single_open(file, exam_show, NULL)); } 注意要使用single_open而不是seq_open。 3. 定义struct file_operations结构 struct file_operations exam_single_seq_file_operations = {         .open           = exam_single_open,         .read           = seq_read,         .llseek         = seq_lseek,         .release        = single_release, }; 注意,如果open函数使用了single_open,release函数必须为single_release,而不是seq_release。在源代码包中给出了一个使用seq_file的具体例子seqfile_exam.c,它使用seq_file提供了一个查看当前系统运行的所有进程的/proc接口,在编译并插入该模块后,用户通过命令"cat /proc/ exam_esq_file"可以查看系统的所有进程。   seq_file的实现其实很简单,就是/proc下的一个文件,不过它不使用proc_file_read(),而是使用seq_read()函数进行数据的读取,并且还提供了一些函数以方便数据的格式化读取。     附录1四个重要的数据结构   图F-1  4个重要的数据结构 其中 struct file {        /*         * fu_list becomes invalid after file_free is called and queued via         * fu_rcuhead for RCU freeing         */        union {               struct list_head    fu_list;               struct rcu_head fu_rcuhead;        } f_u;        struct path           f_path; #define f_dentry f_path.dentry #define f_vfsmnt       f_path.mnt        const struct file_operations      *f_op;        spinlock_t            f_lock;  /* f_ep_links, f_flags, no IRQ */        atomic_long_t            f_count;        unsigned int              f_flags;        fmode_t                     f_mode;        loff_t                   f_pos;        struct fown_struct      f_owner;        const struct cred  *f_cred;        struct file_ra_state     f_ra;        u64               f_version; #ifdef CONFIG_SECURITY        void                     *f_security; #endif        /* needed for tty driver, and maybe others */        void                     *private_data; #ifdef CONFIG_EPOLL        /* Used by fs/eventpoll.c to link all the hooks to this file */        struct list_head    f_ep_links; #endif /* #ifdef CONFIG_EPOLL */        struct address_space  *f_mapping; #ifdef CONFIG_DEBUG_WRITECOUNT        unsigned long f_mnt_write_state; #endif }; /*  * NOTE:  * read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl  * can be called without the big kernel lock held in all filesystems.  */ struct file_operations {        struct module *owner;        loff_t (*llseek) (struct file *, loff_t, int);        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);        ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);        ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);        int (*readdir) (struct file *, void *, filldir_t);        unsigned int (*poll) (struct file *, struct poll_table_struct *);        int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);        int (*mmap) (struct file *, struct vm_area_struct *);        int (*open) (struct inode *, struct file *);        int (*flush) (struct file *, fl_owner_t id);        int (*release) (struct inode *, struct file *);        int (*fsync) (struct file *, int datasync);        int (*aio_fsync) (struct kiocb *, int datasync);        int (*fasync) (int, struct file *, int);        int (*lock) (struct file *, int, struct file_lock *);        ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);        unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);        int (*check_flags)(int);        int (*flock) (struct file *, int, struct file_lock *);        ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);        ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);        int (*setlease)(struct file *, long, struct file_lock **); }; struct dentry {        atomic_t d_count;        unsigned int d_flags;        /* protected by d_lock */        spinlock_t d_lock;             /* per dentry lock */        int d_mounted;        struct inode *d_inode;             /* Where the name belongs to - NULL is                                     * negative */        /*         * The next three fields are touched by __d_lookup.  Place them here         * so they all fit in a cache line.         */        struct hlist_node d_hash;  /* lookup hash list */        struct dentry *d_parent;   /* parent directory */        struct qstr d_name;        struct list_head d_lru;              /* LRU list */        /*         * d_child and d_rcu can share memory         */        union {               struct list_head d_child;   /* child of parent list */              struct rcu_head d_rcu;        } d_u;        struct list_head d_subdirs;       /* our children */        struct list_head d_alias;    /* inode alias list */        unsigned long d_time;             /* used by d_revalidate */        const struct dentry_operations *d_op;        struct super_block *d_sb; /* The root of the dentry tree */        void *d_fsdata;                 /* fs-specific data */        unsigned char d_iname[DNAME_INLINE_LEN_MIN];       /* small names */ }; struct inode {        struct hlist_node i_hash;        struct list_head    i_list;            /* backing dev IO list */        struct list_head    i_sb_list;        struct list_head    i_dentry;        unsigned long             i_ino;        atomic_t              i_count;        unsigned int        i_nlink;        uid_t                    i_uid;        gid_t                    i_gid;        dev_t                   i_rdev;        unsigned int        i_blkbits;        u64               i_version;        loff_t                   i_size; #ifdef __NEED_I_SIZE_ORDERED        seqcount_t           i_size_seqcount; #endif        struct timespec           i_atime;        struct timespec           i_mtime;        struct timespec           i_ctime;        blkcnt_t        i_blocks;        unsigned short          i_bytes;        umode_t                     i_mode;        spinlock_t            i_lock;   /* i_blocks, i_bytes, maybe i_size */        struct mutex        i_mutex;        struct rw_semaphore  i_alloc_sem;        const struct inode_operations  *i_op;        const struct file_operations      *i_fop;   /* former ->i_op->default_file_ops */        struct super_block      *i_sb;        struct file_lock    *i_flock;        struct address_space  *i_mapping;        struct address_space  i_data; #ifdef CONFIG_QUOTA        struct dquot         *i_dquot[MAXQUOTAS]; #endif        struct list_head    i_devices;        union {               struct pipe_inode_info      *i_pipe;               struct block_device    *i_bdev;               struct cdev          *i_cdev;        };        __u32                  i_generation; #ifdef CONFIG_FSNOTIFY        __u32                  i_fsnotify_mask; /* all events this inode cares about */        struct hlist_head  i_fsnotify_mark_entries; /* fsnotify mark entries */ #endif #ifdef CONFIG_INOTIFY        struct list_head    inotify_watches; /* watches on this inode */        struct mutex        inotify_mutex;    /* protects the watches list */ #endif          unsigned long             i_state;        unsigned long             dirtied_when;      /* jiffies of first dirtying */        unsigned int        i_flags;        atomic_t              i_writecount; #ifdef CONFIG_SECURITY        void                     *i_security; #endif #ifdef CONFIG_FS_POSIX_ACL        struct posix_acl   *i_acl;        struct posix_acl   *i_default_acl; #endif        void                     *i_private; /* fs or device private pointer */ };

上一篇:linux rsync同步设置详细指南
下一篇:Servlet3.0引入的新特性一览

相关文章

相关评论