要解决的问题:
- struct inode 和 struct file,cdev 和 inode的关系
- open系统调用是如何通过设备号来找到
一、open概述
使用open函数打开设备文件,到底做了些什么工作?下图中列出了open函数执行的大致过程。
二、字符设备的注册
linux内核cdev_init系列函数。
内核中每个字符设备都对应一个 cdev 结构的变量,下面是它的定义:
struct cdev {
struct kobject kobj; // 每个 cdev 都是一个 kobject
struct module *owner; // 指向实现驱动的模块
const struct file_operations *ops; // 操纵这个字符设备文件的方法
struct list_head list; // 与 cdev 对应的字符设备文件的 inode->i_devices 的链表头
dev_t dev; // 起始设备编号
unsigned int count; // 设备范围号大小
};
一个 cdev 一般它有两种定义初始化方式:静态的和动态的。
静态内存定义初始化:
struct cdev my_cdev;
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
动态内存定义初始化:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;
两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的数据结构需求而定。
下面贴出了两个函数的代码,以具体看一下它们之间的差异。
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
由此可见,两个函数完成都功能基本一致,只是 cdev_init() 还多赋了一个 cdev->ops 的值。
初始化 cdev 后,需要把它添加到系统中去。为此可以调用 cdev_add() 函数。传入 cdev 结构的指针,起始设备编号,以及设备编号范围。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
内核中所有都字符设备都会记录在一个 kobj_map 结构的 cdev_map 变量中。这个结构的变量中包含一个散列表用来快速存取所有的对象。kobj_map() 函数就是用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里。当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。
kobj_map函数中哈希表的实现原理和前面注册分配设备号中的几乎完全一样,通过要加入系统的设备的主设备号major(major=MAJOR(dev))来获得probes数组的索引值i(i = major % 255),然后把一个类型为struct probe的节点对象加入到probes[i]所管理的链表中,如图2-6所示。其中struct probe所在的矩形块中的深色部分是我们重点关注的内容,记录了当前正在加入系统的字符设备对象的有关信息。其中,dev是它的设备号,range是从次设备号开始连续的设备数量,data是一void *变量,指向当前正要加入系统的设备对象指针p。图2-6展示了两个满足主设备号major % 255 = 2的字符设备通过调用cdev_add之后,cdev_map所展现出来的数据结构状态。
所以,简单地说,设备驱动程序通过调用cdev_add把它所管理的设备对象的指针嵌入到一个类型为struct probe的节点之中,然后再把该节点加入到cdev_map所实现的哈希链表中。
对系统而言,当设备驱动程序成功调用了cdev_add之后,就意味着一个字符设备对象已经加入到了系统,在需要的时候,系统就可以找到它。对用户态的程序而言,cdev_add调用之后,就已经可以通过文件系统的接口呼叫到我们的驱动程序。
当一个字符设备驱动不再需要的时候(比如模块卸载),就可以用 cdev_del() 函数来释放 cdev 占用的内存。
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
其中 cdev_unmap() 调用 kobj_unmap() 来释放 cdev_map 散列表中的对象。kobject_put() 释放 cdev 结构本身。
二、创建inode
设备文件通常在开机启动时自动创建的,不过,我们仍然可以使用命令mknod来创建一个新的设备文件,命令的基本语法如下:
mknod 设备名 设备类型 主设备号 次设备号
当我们使用上述命令,创建了一个字符设备文件时,实际上就是创建了一个设备节点inode结构体,并且将该设备的设备编号记录在成员i_rdev,将成员i_fop指针指向了def_chr_fops结构体。这就是mknod负责的工作内容,具体代码见如
命令mknod最终会调用init_special_inode函数,由于我们创建的是字符设备,因此,会执行第22~23行的代码。这样就完成了上图的内容。
static struct inode *shmem_get_inode(struct super_block *sb, const struct inode *dir,umode_t mode, dev_t dev, unsigned long flags)
{
inode = new_inode(sb);
if (inode) {
......
switch (mode & S_IFMT) {
default:
inode->i_op = &shmem_special_inode_operations;
init_special_inode(inode, mode, dev);
break;
......
}
} else
shmem_free_inode(sb);
return inode;
}
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
inode->i_mode = mode;
if (S_ISCHR(mode)) {
inode->i_fop = &def_chr_fops;
inode->i_rdev = rdev;
}
....
}
四、打开文件流程
当应用层通过open api打开一个文件,内核中究竟如何处理? 本身用来描述内核中对应open 系统调用的处理流程。
4.1 数据结构
struct fdtable 一个进程可以打开很多文件, 内核用fdtable来管理这些文件。
include/linux/fdtable.h
struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
unsigned long *full_fds_bits;
struct rcu_head rcu;
};
fd: 文件描述符数组
open_fds: 为方便查找数组中的空闲项, 为该数组建立的位图
close_on_exec: 在打开的文件中, 有些文件时用于执行目的, 在执行完成之后应该自动关闭
struct files_struct 对于大多数进程, 打开文件的数量是有限的,一种优化的设计方式是为每个进程内置分配少量数目的文件描述符指针数组, 但进程需要更多的指针时, 再动态扩展。 为此, 进程并不直接使用fdtable, 而是使用files_struct结构体, 作为task_struct的一个域.fdt指向进程实际使用的fdtable。 对于大多数进程来说, 打开文件的梳理并不会很多, 这时候无需另外分配空间, 直接指向内嵌的结构, 即fdtab域。
/*
* Open file table structure
*/
struct files_struct {
/*
* read mostly part
*/
atomic_t count;
bool resize_in_progress;
wait_queue_head_t resize_wait;
struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp;
unsigned int next_fd;
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1];
unsigned long full_fds_bits_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};
struct file每个打开的文件都会对应一个file结构体, 进程通过它对文件进行操作。
include/linux/fs.h
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
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;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err; /* for syncfs */
} __randomize_layout
f_path: 文件路径
f_op: 指向文件操作表, read/write等操作都会调用这里的回调
f_mapping: 指向文件地址空间描述符
f_pos: 当前文件的偏移值
struct inode
VFS inode包含文件访问权限、属主、组、大小、生成时间、访问时间、最后修改时间等信息。它是linux管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。inode结构中的静态信息取自物理设备上的文件系统,由文件系统指定的函数填写,它只存在于内存中,可以通过inode缓存访问。虽然每个文件都有相应的inode结点,但是只有在需要的时候系统才会在内存中为其建立相应的inode数据结构,建立的inode结构将形成一个链表,我们可以通过遍历这个链表去得到我们需要的文件结点,VFS也为已分配的inode构造缓存和哈希表,以提 高系统性能。inode结构中的struct inode_operations *i_op为我们提供了一个inode操作列表,通过这个列表提供的函数我们可以对VFS inode结点进行各种操作。每个inode结构都有一个i结点号i_ino,在同一个文件系统中每个i结点号是唯一的
struct inode {
struct list_headi_hash;
struct list_headi_list;
struct list_headi_dentry;
struct list_headi_dirty_buffers;
unsigned longi_ino; /*每一个inode都有一个序号,经由super block结构和其序号,我们可以很轻易的找到这个inode。*/
atomic_t i_count; /*在Kernel里,很多的结构都会记录其reference count,以确保如果某个结构正在使用,它不会被不小心释放掉,i_count就是其reference count。*/
kdev_t i_dev; /* inode所在的device代码 */
umode_t i_mode; /* inode的权限 */
nlink_t i_nlink; /* hard link的个数 */
uid_t i_uid; /* inode拥有者的id */
gid_t i_gid; /* inode所属的群组id */
kdev_t i_rdev; /* 如果inode代表的是device的话,那此字段将记录device的代码 */
...............
}
4.2 处理流程
4.2.1 open系统调用
整体调用栈
#3 0xffffffff81218174 in do_filp_open (dfd=dfd@entry=-100, pathname=pathname@entry=0xffff888004950000, op=op@entry=0xffffc90000173ee4) at fs/namei.c:3396
#4 0xffffffff81203cfd in do_sys_openat2 (dfd=-100, filename=<optimized out>, how=how@entry=0xffffc90000173f20) at fs/open.c:1168
#5 0xffffffff81205135 in do_sys_open (dfd=<optimized out>, filename=<optimized out>, flags=<optimized out>, mode=<optimized out>) at fs/open.c:1184
#6 0xffffffff819bf903 in do_syscall_64 (nr=<optimized out>, regs=0xffffc90000173f58) at arch/x86/entry/common.c:46
#7 0xffffffff81a0007c in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:120
当我们在用户空间调用open之后,会产生一个软中断,然后通过系统调用进入内核空间。通过系统调用号,我们就可以跳转到该中断例程的入口地址
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)
{
long ret;
/*检查是否应该不考虑用户层传递的标志、总是强行设置
O_LARGEFILE标志。如果底层处理器的字长不是32位,就是这种情况*/
if (force_o_largefile())
flags |= O_LARGEFILE;
/*实际工作*/
ret = do_sys_open(AT_FDCWD, filename, flags, mode);
/* avoid REGPARM breakage on x86: */
asmlinkage_protect(3, ret, filename, flags, mode);
return ret;
}
我们看下*SYSCALL_DEFINE3(open, const char __user , filename, int, flags, int, mode) 展开是怎么样的
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
再看下SYSCALL_DEFINEx
#define SYSCALL_DEFINEx(x, sname, ...) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
再看下__SYSCALL_DEFINEx
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))
这里对对应__SC_DECL3
#define __SC_DECL1(t1, a1) t1 a1
#define __SC_DECL2(t2, a2, ...) t2 a2, __SC_DECL1(__VA_ARGS__)
#define __SC_DECL3(t3, a3, ...) t3 a3, __SC_DECL2(__VA_ARGS__)
这们一步步展开SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)代替进去,可以得到
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)
= SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
=asmlinkage long sys_open(__SC_DECL3(__VA_ARGS__))
=asmlinkage long sys_open(const char __user* filename, int flags, int mode)
这个才是真正的函数原型
在sys_open里面继续调用do_sys_open完成 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)) {
/*在内核中,每个打开的文件由一个文件描述符表示该描述符在特定于进程的数组中充当位置索引(数组是
task_struct->files->fd_arry),该数组的元素包含了file结构,其中包括每个打开文件的所有必要信息。因此,调用下面
函数查找一个未使用的文件描述符,返回的是上面说的数组的下标*/
fd = get_unused_fd_flags(flags);
if (fd >= 0) {
/*fd获取成功则开始打开文件,此函数是主要完成打开功能的函数*/
//如果分配fd成功,则创建一个file对象
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()函数,根据inode所指定的信息进行打开
函数(参数为f)将该文件加入到文件监控的系统中。该系统是用来监控文件被打开,创建,
读写,关闭,修改等操作的*/
fsnotify_open(f->f_path.dentry);
/*将文件指针安装在fd数组中
将struct file *f加入到fd索引位置处的数组中。如果后续过程中,有对该文件描述符的
操作的话,就会通过查找该数组得到对应的文件结构,而后在进行相关操作。*/
fd_install(fd, f);
}
}
putname(tmp);
return fd;
}
该函数主要分为如下几个步骤来完成打开文件的操作:
1.将文件名参数从用户态拷贝至内核,调用函数get_name();
2.从进程的文件表中找到一个空闲的文件表指针,调用了函数get_unused_fd_flgas();
3.完成真正的打开操作,调用函数do_filp_open();
4.将打开的文件添加到进程的文件表数组中,调用函数fd_install();
getname函数主要的任务是将文件名filename从用户态拷贝至内核态
char * getname(const char __user * filename)
{
char *tmp, *result;
result = ERR_PTR(-ENOMEM);
tmp = __getname(); //从内核缓存中分配空间;
if (tmp) {
//将文件名从用户态拷贝至内核态;
int retval = do_getname(filename, tmp);
result = tmp;
if (retval){
__putname(tmp);
result = ERR_PTR(retval);
}
}
audit_getname(result);
return result;
}
get_unused_fd_flags实际调用的是alloc_fd
#define get_unused_fd_flags(flags) alloc_fd(0, (flags))
/*
* allocate a file descriptor, mark it busy.
*/
int alloc_fd(unsigned start, unsigned flags)
{
struct files_struct *files = current->files;//获得当前进程的files_struct 结构
unsigned int fd;
int error;
struct fdtable *fdt;
spin_lock(&files->file_lock);
repeat:
fdt = files_fdtable(files);
fd = start;
if (fd next_fd) //从上一次打开的fd的下一个fd开始搜索空闲的fd
fd = files->next_fd;
if (fd max_fds)//寻找空闲的fd,返回值为空闲的fd
fd = find_next_zero_bit(fdt->open_fds->fds_bits,
fdt->max_fds, fd);
//如果有必要,即打开的fd超过max_fds,则需要expand当前进程的fd表;
//返回值error<0表示出错,error=0表示无需expand,error=1表示进行了expand;
error = expand_files(files, fd);
if (error)
goto out;
/*
* If we needed to expand the fs array we
* might have blocked - try again.
*/
//error=1表示进行了expand,那么此时需要重新去查找空闲的fd;
if (error)
goto repeat;
//设置下一次查找的起始fd,即本次找到的空闲的fd的下一个fd,记录在files->next_fd中;
if (start <= files->next_fd)
files->next_fd = fd + 1;
FD_SET(fd, fdt->open_fds);
if (flags & O_CLOEXEC)
FD_SET(fd, fdt->close_on_exec);
else
FD_CLR(fd, fdt->close_on_exec);
error = fd;
#if 1
/* Sanity check */
if (rcu_dereference(fdt->fd[fd]) != NULL) {
printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
rcu_assign_pointer(fdt->fd[fd], NULL);
}
#endif
out:
spin_unlock(&files->file_lock);
return error;
}
该函数为需要打开的文件在当前进程内分配一个空闲的文件描述符fd,该fd就是open()系统调用的返回值
do_filp_open函数的一个重要作用就是根据传递近来的权限进行分析,并且分析传递近来的路径名字,根据路径名逐个解析成dentry,并且通过dentry找到inode,inode就是记录着该文件相关的信息, 包括文件的创建时间和文件属性所有者等等信息,根据这些信息就可以找到对应的文件操作方法。在这个过程当中有一个临时的结构体用于保存在查找过程中的相关信息
do_file_open 函数的处理如下, 主要调用了path_openat 函数去执行真正的open 流程:
fs/namei.c
do_sys_open->do_sys_openat2->do_filp_open
struct file *do_filp_open(int dfd, struct filename *pathname,
const struct open_flags *op)
{
struct nameidata nd;
int flags = op->lookup_flags;
struct file *filp;
set_nameidata(&nd, dfd, pathname);
filp = path_openat(&nd, op, flags | LOOKUP_RCU);
if (unlikely(filp == ERR_PTR(-ECHILD)))
filp = path_openat(&nd, op, flags);
if (unlikely(filp == ERR_PTR(-ESTALE)))
filp = path_openat(&nd, op, flags | LOOKUP_REVAL);
restore_nameidata();
return filp;
}
path_openat: 执行open的核心流程
fs/namei.c
do_sys_open->do_sys_openat2->do_filp_open->path_openat
static struct file *path_openat(struct nameidata *nd,
const struct open_flags *op, unsigned flags)
{
struct file *file;
int error;
file = alloc_empty_file(op->open_flag, current_cred()); /* 1 */
if (IS_ERR(file))
return file;
if (unlikely(file->f_flags & __O_TMPFILE)) {
error = do_tmpfile(nd, flags, op, file);
} else if (unlikely(file->f_flags & O_PATH)) {
error = do_o_path(nd, flags, file);
} else {
const char *s = path_init(nd, flags);
while (!(error = link_path_walk(s, nd)) && /* 2 */
(s = open_last_lookups(nd, file, op)) != NULL) /* 3 */
;
if (!error)
error = do_open(nd, file, op); /* 4 */
terminate_walk(nd);
}
if (likely(!error)) {
if (likely(file->f_mode & FMODE_OPENED))
return file;
WARN_ON(1);
error = -EINVAL;
}
fput(file);
if (error == -EOPENSTALE) {
if (flags & LOOKUP_RCU)
error = -ECHILD;
else
error = -ESTALE;
}
return ERR_PTR(error);
}
(1) 申请 file 结构体, 并做初始化
(2) 找到路径的最后一个分量
(3) 对于最后一个分量进行处理, 这里面会去查找文件是否存在,如果不存在则看条件创建
(4) 执行open的最后步骤, 例如调用open 回调
我们使用的open函数在内核中对应的是sys_open函数,sys_open函数又会调用do_sys_open函数。在do_sys_open函数中,首先调用函数get_unused_fd_flags来获取一个未被使用的文件描述符fd,该文件描述符就是我们最终通过open函数得到的值。紧接着,又调用了do_filp_open函数,该函数通过调用函数get_empty_filp得到一个新的file结构体,之后的代码做了许多复杂的工作,如解析文件路径,查找该文件的文件节点inode等,直接来到了函do_dentry_open函数,如下所示
fs/open.c
do_sys_open->do_sys_openat2->do_filp_open->path_openat->do_open->vfs_open
int vfs_open(const struct path *path, struct file *file)
{
file->f_path = *path;
return do_dentry_open(file, d_backing_inode(path->dentry), NULL);
}
static int do_dentry_open(struct file *f,
struct inode *inode,
int (*open)(struct inode *, struct file *))
{
static const struct file_operations empty_fops = {};
int error;
path_get(&f->f_path);
f->f_inode = inode;
f->f_mapping = inode->i_mapping;
f->f_wb_err = filemap_sample_wb_err(f->f_mapping);
f->f_sb_err = file_sample_sb_err(f); /* 1 */
if (unlikely(f->f_flags & O_PATH)) {
f->f_mode = FMODE_PATH | FMODE_OPENED;
f->f_op = &empty_fops;
return 0;
}
if (f->f_mode & FMODE_WRITE && !special_file(inode->i_mode)) {
error = get_write_access(inode);
if (unlikely(error))
goto cleanup_file;
error = __mnt_want_write(f->f_path.mnt);
if (unlikely(error)) {
put_write_access(inode);
goto cleanup_file;
}
f->f_mode |= FMODE_WRITER;
}
/* POSIX.1-2008/SUSv4 Section XSI 2.9.7 */
if (S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode))
f->f_mode |= FMODE_ATOMIC_POS;
f->f_op = fops_get(inode->i_fop); /*取该文件节点inode的成员变量i_fop*/
if (WARN_ON(!f->f_op)) {
error = -ENODEV;
goto cleanup_all;
}
error = security_file_open(f);
if (error)
goto cleanup_all;
error = break_lease(locks_inode(f), f->f_flags);
if (error)
goto cleanup_all;
/* normally all 3 are set; ->open() can clear them if needed */
f->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
if (!open)
open = f->f_op->open;
if (open) {
error = open(inode, f); /* 3 */
if (error)
goto cleanup_all;
}
f->f_mode |= FMODE_OPENED;
if ((f->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ)
i_readcount_inc(inode);
if ((f->f_mode & FMODE_READ) &&
likely(f->f_op->read || f->f_op->read_iter))
f->f_mode |= FMODE_CAN_READ;
if ((f->f_mode & FMODE_WRITE) &&
likely(f->f_op->write || f->f_op->write_iter))
f->f_mode |= FMODE_CAN_WRITE;
f->f_write_hint = WRITE_LIFE_NOT_SET;
f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);
/* NB: we're sure to have correct a_ops only after f_op->open */
if (f->f_flags & O_DIRECT) {
if (!f->f_mapping->a_ops || !f->f_mapping->a_ops->direct_IO)
return -EINVAL;
}
/*
* XXX: Huge page cache doesn't support writing yet. Drop all page
* cache for this file before processing writes.
*/
if ((f->f_mode & FMODE_WRITE) && filemap_nr_thps(inode->i_mapping))
truncate_pagecache(inode, 0);
return 0;
cleanup_all:
if (WARN_ON_ONCE(error > 0))
error = -EINVAL;
fops_put(f->f_op);
if (f->f_mode & FMODE_WRITER) {
put_write_access(inode);
__mnt_drop_write(f->f_path.mnt);
}
cleanup_file:
path_put(&f->f_path);
f->f_path.mnt = NULL;
f->f_path.dentry = NULL;
f->f_inode = NULL;
return error;
}
def_chr_fops结构体(位于内核源码/fs/char_dev.c文件)
const struct file_operations def_chr_fops = {
.open = chrdev_open,
.llseek = noop_llseek,
};
(1) (2) 设置file结构体的一些成员
(3) 找到open 回调, 并执行
以上代码中的使用fops_get函数来获取该文件节点inode的成员变量i_fop,在上图中我们使用mknod创建字符设备文件时,将def_chr_fops结构体赋值给了该设备文件inode的i_fop成员。到了这里,我们新建的file结构体的成员f_op就指向了def_chr_fops。
最终,会执行def_chr_fops中的open函数,也就是chrdev_open函数,可以理解为一个字符设备的通用初始化函数,根据字符设备的设备号,找到相应的字符设备,从而得到操作该设备的方法,代码实现如下。
chrdev_open函数(位于内核源码/fs/char_dev.c文件)
static int chrdev_open(struct inode *inode, struct file *filp)
{
const struct file_operations *fops;
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);
if (!kobj)
return -ENXIO;
new = container_of(kobj, struct cdev, 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;
fops = fops_get(p->ops);
if (!fops)
goto out_cdev_put;
replace_fops(filp, fops);
if (filp->f_op->open) {
ret = filp->f_op->open(inode, filp);
if (ret)
goto out_cdev_put;
}
return 0;
out_cdev_put:
cdev_put(p);
return ret;
}
在Linux内核中,使用结构体cdev来描述一个字符设备。在以上代码中的第14行,inode->i_rdev中保存了字符设备的设备编号,通过函数kobj_lookup函数便可以找到该设备文件cdev结构体的kobj成员,再通过函数container_of便可以得到该字符设备对应的结构体cdev。函数container_of的作用就是通过一个结构变量中一个成员的地址找到这个结构体变量的首地址。同时,将cdev结构体记录到文件节点inode中的i_cdev,便于下次打开该文件。继续阅读第36~45行代码,我们可以发现,函数chrdev_open最终将该文件结构体file的成员f_op替换成了cdev对应的ops成员,并执行ops结构体中的open函数。
最后,调用上图的fd_install函数,完成文件描述符和文件结构体file的关联,之后我们使用对该文件描述符fd调用read、write函数,最终都会调用file结构体对应的函数,实际上也就是调用cdev结构体中ops结构体内的相关函数
————————————————
版权声明:本文为CSDN博主「大城市的小蜗牛」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_46535940/article/details/124585580
假设有一个点灯的应用程序,这个程序所需要做的工作为:
1.读写文件
2.点灯,获取按键等等
在应用层有:
open read write (由c库实现)
驱动层有:
led_open led_read led_write
问题:应用层(open,read,write)是如何调用到底层驱动的led_open,led_read,led_write 等驱动函数的呢??
解释:
在驱动层和应用层之间有个系统调用接口:
sys_open sys_read sys_write,当应用层调用(open,read,write)函数时,会发出一个swi val的异常命令,系统调用接口会根据val的值判断异常的原因来调用相应的函数sys_open,sys_read,sys_write.这个函数会根据打开不同的文件节点来最终调用相应的底层驱动程序
下面分析下sys_open的流程:
在si中搜索sys_open,发现在arch\arm\kernel\calls.S下面有这个的形式:
CALL(sys_read)
CALL(sys_write)
/* 5 */ CALL(sys_open)
CALL(sys_close)
这个sys_open前面有个5的注释,猜测这个5就是前面所说的swi val中的val的值( 具体情况以后分析,这里只是猜测)
在这个文件中定义了300多个系统调用。
sys_open的定义如下:
asmlinkage long sys_open(const char __user *filename,
int flags, int mode);
问题:asmlinkage的意思是什么?
函数定义前加宏asmlinkage ,表示这些函数通过堆栈而不是通过寄存器传递参数。
gcc编译器在汇编过程中调用c语言函数时传递参数有两种方法:一种是通过堆栈,另一种是通过寄存器。缺省时采用寄存器,假如你要在你的汇编过程中调用c语言函数,并且想通过堆栈传递参数,你定义的c函数时要在函数前加上宏asmlinkage
sys_open的实现如下:
asmlinkage long sys_open(const char __user *filename, int flags, int mode)
{
long ret;
if (force_o_largefile()) //查看打开的是否是大文件,如果是的话,置大文件标志位:O_LARGEFILE
flags |= O_LARGEFILE;
ret = do_sys_open(AT_FDCWD, filename, flags, mode);
/* avoid REGPARM breakage on x86: */
prevent_tail_call(ret); //检查do_sys_open的调用返回值ret是否有效,ret也就是文件描述符。
return ret;
}
#define AT_FDCWD -100 /* Special value used to indicate openat should use the current working directory. */
AT_FDCWD:这是一个特殊的值用来指定openat应该使用当前目录,注释这样写的,看不懂。。。
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(); //取得系统中可用的文件描述符fd。
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, flags, mode);
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;
}
下面看下:
do_filp_open的实现:
static struct file *do_filp_open(int dfd, const char *filename, int flags,
int mode)
{
int namei_flags, error;
struct nameidata nd;
namei_flags = flags;
if ((namei_flags+1) & O_ACCMODE) // 如果flags有O_WRONLY,则增加O_RDONLY
namei_flags++;
error = open_namei(dfd, filename, namei_flags, mode, &nd); // open_namei函数主要执行文件操作的inode部分的打开等操作比如目录。
if (!error)
return nameidata_to_filp(&nd, flags);//把文件的inode相关信息转换成文件结构。
return ERR_PTR(error);
}
在分析之前,先看个struct nameidata 结构体:
struct nameidata {
struct dentry *dentry; //目录数据
struct vfsmount *mnt; // 虚拟文件挂载点数据
struct qstr last; // hash值
unsigned int flags; // 文件操作标识
int last_type; // 类型
unsigned depth;
char *saved_names[MAX_NESTED_LINKS + 1];
/* Intent data */
union {
struct open_intent open;
} intent;// 专用数据
};
struct file *nameidata_to_filp(struct nameidata *nd, int flags)
{
struct file *filp;
/* Pick up the filp from the open intent */
filp = nd->intent.open.file; // 把相关 file结构的指针赋予 filp。
/* Has the filesystem initialised the file for us? */
if (filp->f_path.dentry == NULL)
filp = __dentry_open(nd->dentry, nd->mnt, flags, filp, NULL); /* ***** 关键函数 ***** */
else
path_release(nd);
return filp;
}
__dentry_open:
static struct file *__dentry_open(struct dentry *dentry, struct vfsmount *mnt,
int flags, struct file *f,
int (*open)(struct inode *, struct file *))
{
struct inode *inode;
int error;
f->f_flags = flags;
f->f_mode = ((flags+1) & O_ACCMODE) | FMODE_LSEEK |
FMODE_PREAD | FMODE_PWRITE;
inode = dentry->d_inode;//获取到inode,打开文件的inode,在open_namei做的赋值
if (f->f_mode & FMODE_WRITE) {
error = get_write_access(inode);
if (error)
goto cleanup_file;
}
f->f_mapping = inode->i_mapping;
f->f_path.dentry = dentry;
f->f_path.mnt = mnt;
f->f_pos = 0;
f->f_op = fops_get(inode->i_fop);//获取字符设备的fops
file_move(f, &inode->i_sb->s_files);
if (!open && f->f_op)
open = f->f_op->open; // // 在这里将open赋为chrdev_open。 // 此处获得 def_chr_fops
if (open) {
error = open(inode, f);//做chrdev_open操作
if (error)
goto cleanup_all;
}
f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);
/* NB: we're sure to have correct a_ops only after f_op->open */
if (f->f_flags & O_DIRECT) {
if (!f->f_mapping->a_ops ||
((!f->f_mapping->a_ops->direct_IO) &&
(!f->f_mapping->a_ops->get_xip_page))) {
fput(f);
f = ERR_PTR(-EINVAL);
}
}
return f;
cleanup_all:
fops_put(f->f_op);
if (f->f_mode & FMODE_WRITE)
put_write_access(inode);
file_kill(f);
f->f_path.dentry = NULL;
f->f_path.mnt = NULL;
cleanup_file:
put_filp(f);
dput(dentry);
mntput(mnt);
return ERR_PTR(error);
}
chrdev_open:
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);
// 执行kobj_lookup函数,在cdev_map里寻找相应的inode->i_rdev设备。
// cdev_map是一个256个probe结构组成的数组,用于查找具有相应设备号的设备。
// inode->i_rdev为设备号,主设备和次设备的结合。
if (!kobj)
return -ENXIO;
new = container_of(kobj, struct cdev, kobj);
/从kobj的位置倒算出cdev的内存地址,获得包含相应kobj的cdev。
spin_lock(&cdev_lock);
p = inode->i_cdev;
if (!p) {
inode->i_cdev = p = new;
// 到这里p已经为我们要的设备cdev了。
inode->i_cindex = idx;
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;
filp->f_op = fops_get(p->ops);
if (!filp->f_op) {
cdev_put(p);
return -ENXIO;
}
if (filp->f_op->open) {
lock_kernel();
ret = filp->f_op->open(inode,filp);
//拿到 cdev操作集。
// 至此以后read,write操作都通过file->f_op直接与我们要的设备操作集挂钩了。
unlock_kernel();
}
if (ret)
cdev_put(p);
return ret;
}
到此,系统通过file->f_op 就与我们在设备驱动里面的定义的相关操作联系起来了,我们之前在写驱动实现的功能操作就被系统通过应用层的open 一步一步的调用到我们自己的open跟相关其他的操作了。
下面看看底层如何注册字符设备的:
以一个按键为例:
register_chrdev(BUTTON_MAJOR, DEVICE_NAME, &s3c24xx_buttons_fops);
/*
**BUTTON_MAJOR:主设备号
**DEVICE_NAME:设备的名字
**&s3c24xx_buttons_fops为button的底层操作函数结构体指针
*/
在分析这个函数之前先看一个结构体:
static struct char_device_struct {
struct char_device_struct *next;//这是一个链表
unsigned int major; //主设备号
unsigned int baseminor; //基本的次设备号
int minorct;
char name[64]; //设备的名字?
struct file_operations *fops; //设备的操作
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
register_chrdev() - Register a major number for character devices.这个函数的意思是注册一个注射被为major的字符设备,其实现如下:
int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
char *s;
int err = -ENOMEM;
cd = __register_chrdev_region(major, 0, 256, name);//好像就是初始化一下char_device_struct,下面具体分析
if (IS_ERR(cd))
return PTR_ERR(cd);
cdev = cdev_alloc();
if (!cdev)
goto out2;
cdev->owner = fops->owner;
cdev->ops = fops; //将底层的操作函数附给struct cdev成员ops
kobject_set_name(&cdev->kobj, "%s", name);
for (s = strchr(kobject_name(&cdev->kobj),'/'); s; s = strchr(s, '/'))
*s = '!';
err = cdev_add(cdev, MKDEV(cd->major, 0), 256);//将这个字符设备加载到系统中,下面分析其实现
if (err)
goto out;
cd->cdev = cdev; //将cdev附到char_device_struct上
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, 0, 256));
return err;
}
__register_chrdev_region的实现:
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
struct char_device_struct *cd, **cp;
int ret = 0;
int i;
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);//分配空间
if (cd == NULL)
return ERR_PTR(-ENOMEM);
mutex_lock(&chrdevs_lock);
/* temporary */
if (major == 0) {//如果设备的major==0(动态分配,在255个主设备id中找到一个未使用的)
for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
if (chrdevs[i] == NULL)
break;
}
if (i == 0) {
ret = -EBUSY;
goto out;
}
major = i;
ret = major;
}
//对char_device_struct进行初始化
cd->major = major; //主设备号
cd->baseminor = baseminor; //次设备号的第一个id
cd->minorct = minorct; //次设备号最大支持的数目
strncpy(cd->name,name, 64); //设备的名字
i = major_to_index(major); //获取索引 == major % 255
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) //遍历chrdev的链表,看看有无不符合条件的
if ((*cp)->major > major ||
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) ||
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;
/* Check for overlapping minor ranges. */
if (*cp && (*cp)->major == major) {
int old_min = (*cp)->baseminor;
int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
int new_min = baseminor;
int new_max = baseminor + minorct - 1;
/* New driver overlaps from the left. */
if (new_max >= old_min && new_max <= old_max) {
ret = -EBUSY;
goto out;
}
/* New driver overlaps from the right. */
if (new_min <= old_max && new_min >= old_min) {
ret = -EBUSY;
goto out;
}
}
cd->next = *cp;
*cp = cd;
mutex_unlock(&chrdevs_lock);
return cd;
out:
mutex_unlock(&chrdevs_lock);
kfree(cd);
return ERR_PTR(ret);
}
cdev_add的实现:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);//将cdev加入到系统中,下面分析实现:
}
kobj_map的实现:
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
struct module *module, kobj_probe_t *probe,
int (*lock)(dev_t, void *), void *data)
{
unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
unsigned index = MAJOR(dev);
unsigned i;
struct probe *p;
if (n > 255)
n = 255;
p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);
if (p == NULL)
return -ENOMEM;
for (i = 0; i < n; i++, p++) {
p->owner = module;
p->get = probe; //这个p->get == exact_match,用于获取指定字符设备的kobj
p->lock = lock;
p->dev = dev;
p->range = range;
p->data = data; //指定设备的cdev的信息
}
mutex_lock(domain->lock);
for (i = 0, p -= n; i < n; i++, p++, index++) {
struct probe **s = &domain->probes[index % 255];
while (*s && (*s)->range < range)
s = &(*s)->next;
p->next = *s;
*s = p;
}
mutex_unlock(domain->lock);
return 0;
}
struct kobj_map结构体:
struct kobj_map {
struct probe {
struct probe *next;//次设备链表
dev_t dev;//由主设备号和次设备号共同组成,每个设备都有单独的dev
unsigned long range;
struct module *owner;
kobj_probe_t *get; //这个参数会在进行系统调用的时候被使用到
int (*lock)(dev_t, void *);
void *data;
} *probes[255]; //这个kobj_map域中可以指示256个次设备的存放
struct mutex *lock;
};
到这里字符设备的注册就已经完成了:
总结一下:
1.初始化char_device_struct,
2. 定义一个 kobj_map 结构的 cdev_map 变量,内核中所有都字符设备都会记录在这个变量中。这个结构的变量中包含一个散列表用来快速存取所有的对象。kobj_map() 函数就是用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里。当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。
搜索关键字:从用户态的open到内核驱动实现流程
————————————————
版权声明:本文为CSDN博主「longshan_2009」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/longshan_2009/article/details/8699067