Linux驱动设计——字符设备驱动(一)

Linux字符设别驱动结构

cdev结构体

struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};

dev_t成员定义了32位的设备号,其中12位为主设备号( 获取主设备号MAJOR(dev_t dev) ),20位为次设备号( 获取次设备号 MINOR(dev_t dev) )。 由主设备号和次设备号获得设备号( MKDEV(int major, int minor) )。

file_operations成员定义了字符设别驱动提供给虚拟文件系统的接口函数。

Linux 2.6 内核提供了一组函数用于操作cdev 结构体:
void cdev_init(struct cdev *, struct file_operations *);

    //cdev_init()函数用于初始化cdev 的成员,并建立cdev 和file_operations 之间的连接
struct cdev *cdev_alloc(void);

    //cdev_alloc()函数用于动态申请一个cdev 内存
void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);

    //cdev_add()函数和cdev_del()函数分别向系统添加和删除一个cdev,完成字符设备的注册和注

销。对cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而对cdev_del()函数的调用则
通常发生在字符设备驱动模块卸载函数中。

分配和释放设备号

在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()或alloc_chrdev_region()函数向系统申请设备号。

int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);

register_chrdev_region()函数用于已知起始设备的设备号的情况,而alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入第一个参数dev 中。alloc_chrdev_region()与register_chrdev_region()对比的优点在于它会自动避开设备号重复的冲突。

相反地,在调用cdev_del()函数从系统注销字符设备之后,unregister_chrdev_region()应该被调用以释放原先申请的设备号,这个函数的原型为:

void unregister_chrdev_region(dev_t from, unsigned count);

Linux 字符设备驱动的组成

在Linux 中,字符设备驱动由如下几个部分组成。

1.字符设备驱动模块加载与卸载函数

在字符设备驱动模块加载函数中应该实现设备号的申请和cdev 的注册,而在卸载函数中应实现设备号的释放和cdev 的注销。

字符设备驱动模块加载与卸载函数模板

1 /* 设备结构体*/
2 struct xxx_dev_t {
3   struct cdev cdev;
4   ...
5 } xxx_dev;
6 /* 设备驱动模块加载函数*/
7 static int _ _init xxx_init(void)
8 {
9   ...
10   cdev_init(&xxx_dev.cdev, &xxx_fops); /* 初始化cdev */
11   xxx_dev.cdev.owner = THIS_MODULE;
12   /* 获取字符设备号*/
13   if (xxx_major) {
14     register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
15   } else {
16     alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
17   }
18
19   ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注册设备*/
20   ...
21 }
22 /*设备驱动模块卸载函数*/
23 static void _ _exit xxx_exit(void)
24 {
25   unregister_chrdev_region(xxx_dev_no, 1); /* 释放占用的设备号*/
26   cdev_del(&xxx_dev.cdev); /* 注销设备*/
27   ...
28 }

2.字符设备驱动的file_operations 结构体中成员函数

字符设备驱动读、写、I/O 控制函数模板

1 /* 读设备*/
2 ssize_t xxx_read(struct file *filp, char __user *buf, size_t count,
3 loff_t*f_pos)
4 {
5   ...
6   copy_to_user(buf, ..., ...);
7   ...
8 }
9 /* 写设备*/
10 ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count,
11 loff_t *f_pos)
12 {
13   ...
14   copy_from_user(..., buf, ...);
15   ...
16 }
17 /* ioctl 函数 */
18 int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd,
19 unsigned long arg)
20 {
21   ...
22   switch (cmd) {
23   case XXX_CMD1:
24     ...
25     break;
26   case XXX_CMD2:
27     ...
28     break;
29   default:
30     /* 不能支持的命令 */
31     return - ENOTTY;
32   }
33   return 0;
34 }

字符设备驱动的结构、字符设备驱动与字符设备以及字符设备驱动与用户空间访问该设备的程序之间的关系:

Linux驱动设计——字符设备驱动(一)

struct inode和struct file详解

内核中用inode结构表示具体的文件,而用file结构表示打开的文件描述符。

inode 译成中文就是索引节点。每个存储设备或存储设备的分区(存储设备是硬盘、软盘、U盘 ... ... )被格式化为文件系统后,应该有两部份,一部份是inode,另一部份是Block,Block是用来存储数据用的。而inode呢,就是用来存储这些数据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。inode为每个文件进行信息索引,所以就有了inode的数值。操作系统根据指令,能通过inode值最快的找到相对应的文件。 
做个比喻,比如一本书,存储设备或分区就相当于这本书,Block相当于书中的每一页,inode 就相当于这本书前面的目录,一本书有很多的内容,如果想查找某部份的内容,我们可以先查目录,通过目录能最快的找到我们想要看的内容。
当我们用ls 查看某个目录或文件时,如果加上-i 参数,就可以看到inode节点了;比如ls -li lsfile.sh ,最前面的数值就是inode信息

重要成员:

dev_t  i_rdev       设备文件的设备号
struct cdev *i_cdev   代表字符设备的数据结构

struct file 代表一个打开的文件描述符,它不是专门给驱动程序使用的,系统中每一个打开的文件在内核中都有一个关联的struct file。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后关闭。当文件的所有实例都关闭之后,内核释放这个数据结构。

重要成员:

void *private_data; 

loff_t f_pos;     //该文件在当前进程中的文件偏移量

private_data是Linux下连接VFS文件系统框架和不同文件/文件系统底层实现之间的一个核心数据结构,虽然它只是一个指针,但是一个指针可以解决所有问题。

因 为file是VFS框架的一个基本概念,它要支持文件操作结构,例如open/read/write/release之类的接口,甚至还有poll等,只有有了这些结构,它们才能被纳入VFS这个大家庭。但是对于不同的设备文件来说,它们只是披着文件外衣的设备,所以他要有自己特有的结构来和设备交流,而 这private_data就是这个连接的纽带。这样说可能还是比较抽象,最后是多看一些代码感受可能会深一些。

实质就是把device设备的private_data指针指向了自己定义的结构体。增加可复用性。

下面是一些使用private_data的文件:

1、tty设备
static ssize_t tty_read(struct file * file, char __user * buf, size_t count, 
            loff_t *ppos)
{
    int i;
    struct tty_struct * tty;
    struct inode *inode;
    struct tty_ldisc *ld;

tty = (struct tty_struct *)file->private_data;
2、tun/tap设备
static ssize_t tun_chr_aio_read(struct kiocb *iocb, const struct iovec *iv,
                unsigned long count, loff_t pos)
{
    struct file *file = iocb->ki_filp;
    struct tun_struct *tun = file->private_data;
3、套接口文件
static ssize_t do_sock_read(struct msghdr *msg, struct kiocb *iocb,
        struct file *file, const struct iovec *iov,
        unsigned long nr_segs)
{
    struct socket *sock = file->private_data;
    size_t size = 0;
4、epoll文件
static int ep_eventpoll_close(struct inode *inode, struct file *file)
{
    struct eventpoll *ep = file->private_data;
5、shm文件
long do_shmat(int shmid, char __user *shmaddr, int shmflg, ulong *raddr)
{
……
    file->private_data = sfd;

struct inode {
struct hlist_node i_hash;    //哈希表 
struct list_head i_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;      //使用id
gid_t i_gid;       //使用者id组
dev_t i_rdev; //该成员表示设备文件的inode结构,它包含了真正的设备编号。
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;
unsigned int i_blkbits;
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; //该成员表示字符设备的内核的 内部结构。当inode指向一个字符设备文件时,该成员包含了指向struct cdev结构的指针,其中cdev结构是字符设备结构体。
};
int i_cindex; __u32 i_generation; #ifdef CONFIG_DNOTIFY
unsigned long i_dnotify_mask; /* Directory notify events */
struct dnotify_struct *i_dnotify; /* for directory notifications */
#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
void *i_private; /* fs or device private pointer */
};

  

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;  //RCU(Read-Copy Update)是Linux 2.6内核中新的锁机制
} f_u;
struct path f_path;      //包含dentry和mnt两个成员,用于确定文件路径
#define f_dentry f_path.dentry //该成员是对应的 目录结构 。
#define f_vfsmnt f_path.mnt
const struct file_operations *f_op; //该操作 是定义文件关联的操作的。内核在执行open时对这个 指针赋值。
atomic_long_t f_count;
unsigned int f_flags; //该成员是文件标志。
mode_t f_mode;    
loff_t f_pos;     //该文件在当前进程中的文件偏移量
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
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;
spinlock_t f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
};

文件操作函数解析

按照惯例, file_operations结构或者指向这类结构的指针称为fops (或者是与此相关的其他叫法) .这个结构中的每一个字段都必须指向驱动程序中实现特定操作的函数,对于不支持的操作,对应的字段可置为NULL 值. 对各个函数而言,如果对应字段被赋为NULL 指针,那么内核的具体处理行为是不尽相同的,本节后面的列表会列出这些差异。

在通读file_operations 方法的清单肘,我们会注意到许多参数包含有__user 字符串,它其实是一种形式的文挡而已, 表明指针是一个用户空间地址,因此不能被直接引用. 对通常的编译来讲, __user 没有任何效果,但是可由外部检查软件使用,用来寻找对用户空间地址的错误使用。

struct module *owner

第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.

loff_t (*llseek) (struct file *, loff_t, int);

参数:打开的文件指针,偏移量,起始地址(SEEK_SET/SEEK_CUR/SEEK_END)

返回值:新的读写地址

llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

参数:设备文件指针,数据输出目标地址,读取字节数,相对偏移地址(默认为0)

返回值:读取字节数

对应的应用层的read函数:int read(int fd, const void *buf, size_t length);

用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).

辅助函数:unsigned long copy_to_user(void __ user *to,const void *from,unsigned long count);

ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);

初始化一个异步读 -- 可能在函数返回前不结束的读操作. 如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地).

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

参数:设备文件指针,数据输入目标地址,写入字节数,相对偏移地址(默认为0)

返回值:输入字节数

应用层函数 int write(int fd,const void *buf, size_t length); 从buf指向的缓冲区中取length个字节写入fd文件中,返回实际写入字节数。

应用层中buf中length长数据写入到驱动层中buf中,关键是偏移量由谁指定?驱动中设置的偏移地址与应用程序中的偏移地址的关系?

就write和read在应用层中的偏移量与驱动层的偏移量是没有关系的(除了使用llseek的关联)。应用层读写函数被调用后,读写指针自动增加;而驱动中需要程序员控制。当应用层中使用lseek函数时,能够控制驱动中的读写指针的读写位置。

 //应用程序
...
while()
{
lseek(dev_fd, , SEEK_SET); //如果改成SEEK_CUR呢?
//会改变驱动中loff_t *offp么?
write(dev_fd,wr_buf,sizeof(wr_buf));
cnt ++;
if(cnt > )break;
usleep();
}
...

发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.

辅助函数:unsigned long copy_from_user(void __ user *to,const void *from,unsigned long count);

关于read、write、*llseek的详细分析:http://blog.chinaunix.net/uid-25014876-id-59418.html

ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *);

初始化设备上的一个异步写.

int (*readdir) (struct file *, void *, filldir_t);

对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.

unsigned int (*poll) (struct file *, struct poll_table_struct *);

poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.

相关内容:

__IOR(type, nr,size); __IOW(type,nr,size); __IOWR(type,nr,size); 命令指定参数的 arg在用户空间和内核空间的传递:

用户空间和内核空间传递数据:get_user;put_user;copy_to_user;copy_from_user

int (*mmap) (struct file *, struct vm_area_struct *);

mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.

int (*open) (struct inode *, struct file *); //个人理解此函数是为了建立设备文件和设备的关联(*private_data实现)。

参数:设备的索引节点,设备文件指针

尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.

int (*flush) (struct file *);

flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.

int (*release) (struct inode *, struct file *);

在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.

int (*fsync) (struct file *, struct dentry *, int);

这个方法是 fsync 系统调用的后端, 用户调用来刷新任何挂着的数据. 如果这个指针是 NULL, 系统调用返回 -EINVAL.

int (*aio_fsync)(struct kiocb *, int);

这是 fsync 方法的异步版本.

int (*fasync) (int, struct file *, int);

这个操作用来通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的主题, 在第 6 章中描述. 这个成员可以是NULL 如果驱动不支持异步通知.

int (*lock) (struct file *, int, struct file_lock *);

lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它.

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

这些方法实现发散/汇聚读和写操作. 应用程序偶尔需要做一个包含多个内存区的单个读或写操作; 这些系统调用允许它们这样做而不必对数据进行额外拷贝. 如果这些函数指针为 NULL, read 和 write 方法被调用( 可能多于一次 ).

ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);

这个方法实现 sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另一个. 例如, 它被一个需要发送文件内容到一个网络连接的 web 服务器使用. 设备驱动常常使 sendfile 为 NULL.

ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

sendpage 是 sendfile 的另一半; 它由内核调用来发送数据, 一次一页, 到对应的文件. 设备驱动实际上不实现 sendpage.

unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中. 这个任务通常由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动可以置这个方法为 NULL.

int (*check_flags)(int)

这个方法允许模块检查传递给 fnctl(F_SETFL...) 调用的标志.

int (*dir_notify)(struct file *, unsigned long);

这个方法在应用程序使用 fcntl 来请求目录改变通知时调用. 只对文件系统有用; 驱动不需要实现 dir_notify.

上一篇:【linux】驱动-7-平台设备驱动


下一篇:Linux内核的LED设备驱动框架【转】