资源描述
内核和用户空间通信第一部分---文件系统方式
本文档的Copyleft归wwwlkk所有,使用GPL发布,可以自由拷贝、转载,转载时请保持文档的完整性,严禁用于任何商业用途。
E-mail: wwwlkk@
来源:
内核和用户空间通信—文件系统方式
(1) 重要的数据结构 1
(2)misc设备实现分析 2
(2.2)使用misc设备实现通信 8
(3) procfs实现分析 9
(4)sysctl文件系统实现分析 13
(5)seq_file 17
附录1 四个重要的数据结构 20
(1) 重要的数据结构
为了更好的理解和描述通信原理,需要先认识一下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个关键的问题:
1. inode和dentry结构是如何建立的?
2. 是根据什么数据建立的?
3. inode和dentry结构对应的是哪些数据和哪些操作函数集?
4. 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_operations proc_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_operations proc_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_operations proc_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_operations proc_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, cou
展开阅读全文