Linux字符设备驱动

目录

1 Linux字符设备驱动结构

1.1 cdev结构体

1.1.1 dev_t dev

1.1.2 struct file_operations 

1.2 Linux设备驱动的组成

1.2.1 字符设备驱动模块的加载与卸载

register_chrdev()与register_chrdev_region()

1.2.2 file_operations中的成员函数

1.2.3 用户空间与内核空间的内存复制

1.3 字符设备驱动的结构

1.4 字符设备节点创建


1 Linux字符设备驱动结构

1.1 cdev结构体

        Linux内核中使用cdev结构体来描述一个字符设备。

1 struct cdev {
2     struct kobject kobj;                    // 内嵌的kobject对象 
3     struct module *owner;                   // 所属模块
4     const struct file_operations *ops;      // 文件操作结构体
5     struct list_head list;                  //linux内核所维护的链表指针
6     dev_t dev;                              //设备号
7     unsigned int count;                     //设备数目
8 };

1.1.1 dev_t dev

        dev_t dev是设备号,为32位,高12位是主设备号低20位是次设备号。因此 Linux系统中主设备号范围为 0~4095。在include/linux/kdev_t.h 中提供了几个关于设备号的操作函数:

1 #define MINORBITS    20 // 表示次设备号位数,一共是 20 位。
2 #define MINORMASK    ((1U << MINORBITS) - 1) // 表示次设备号掩码
3 
4 #define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))   //获得主设备号
5 #define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))    //获得此设备号
6 #define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))          //通过主次设备号生成dev_t

1.1.2 struct file_operations 

        file_operations 结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会被应用程序调用Linux系统调用如open()/read()/write()/close()等时,最终被内核调用。

struct file_operations {
 struct module *owner; // owner表示拥有该结构体的模块的指针,基本都是THIS_MODULE。
 loff_t (*llseek) (struct file *, loff_t, int);  // 用来修改一个文件的当前读写位置,将新位置返回,出错返回负值
 ssize_t (*read) (struct file *, char __user *, size_t, loff_t*); // 从设备读取数据,成功返回读取的字节数,失败返回负值。对应用户空间的read、fread函数
 ssize_t (*write) (struct file *, const char __user *, size_t,loff_t *); // 成功返回写入的字节数,失败返回-EINVAL。对应用户空间的write和fwrite函数
 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); // 异步读linux4.0及之前是aio_read
 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); // 异步写linux4.0及之前是aio_write
 int (*iterate) (struct file *, struct dir_context *);
 unsigned int (*poll) (struct file *, struct poll_table_struct*); /* poll轮询函数,用来查询设备是否可以进行非阻塞的读写,当询问条件未触发时,用户空间的select和poll函数会阻塞*/
 long (*unlocked_ioctl) (struct file *, unsigned int, unsignedlong); /* 对应ioctl和fcntl函数,在32位操作系统上运行32位的程序会使用unlocked_ioctl,而64位操作系统上运行32位程序会对应compat_ioctl */
 long (*compat_ioctl) (struct file *, unsigned int, unsignedlong);
 int (*mmap) (struct file *, struct vm_area_struct *); // 将设备内存映射到进程的虚拟地址空间,对应用户程序的mmap函数
 int (*mremap)(struct file *, struct vm_area_struct *);
 int (*open) (struct inode *, struct file *); // 当用户空间调用系统调用open打开设备文件时,设备驱动的open函数会被执行,当然驱动程序可以不实现这个函数,此时,设备的打开操作默认永远成功。
 int (*flush) (struct file *, fl_owner_t id);
 int (*release) (struct inode *, struct file *); // 对应close函数
 int (*fsync) (struct file *, loff_t, loff_t, int datasync); /* 用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中。 */
 int (*aio_fsync) (struct kiocb *, int datasync); /*与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据 */
 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 **, void**);
 long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
 void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
    unsigned (*mmap_capabilities)(struct file *);
#endif
}

1.2 Linux设备驱动的组成

1.2.1 字符设备驱动模块的加载与卸载

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

        Linux内核的编码习惯是为设备定义一个设备相关的结构体,该结构体应包含设备所涉及的cdev、私有数据及锁等信息。如下:

#define DEV_NAME "xxxxx" // 定义设备名

/* 设备自定义结构体 */
struct xxx_dev_t {
    struct cdev cdev;
    ......
};

static int xxx_major = 0;           // 定义一个主设备号,0用于动态分配设备号,大于零用于静态分配设备号
static struct xxx_dev_t xxx_cdev; 


/* 设备驱动模块加载函数 */
static int __init xxx_init(void)
{
    .......
    cdev_init(&xxx_dev.cdev, &xxx_fops); // 初始化cdev
    xxx_dev.owner = THIS_MODULE;

    dev_t xxx_dev_no;   // 定义一个设备号

    /* 获取字符设备号 */
    if (xxx_major)
    {
        xxx_dev_no = MKDEV(xxx_major,0);
        /* 静态分配设备号 */
        register_chrdev_region(xxx_dev_no, 1, DEV_NAME); // 是register_chrdev()的升级版
    } else {
        /*                   : alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
         * @description      : 动态分配设备号
         * @param – dev      : 传出参数,用来保存申请到的设备号。
         * @param - baseminor: 次设备号起始地址,一般 baseminor 为 0,也就是说次设备号从 0 开始。申请多个设备号时用
         * @param - count    : 要申请的设备号数量。
         * @param - name     : 设备名字
         * @return           : 0 成功;其他 失败
         */
        alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
        xxx_major=MAJOR(xxx_dev_no);
    }

    ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); // 注册设备
    ......
}

/* 设备驱动模块卸载函数 */
static void __exit xxx_exit(void)
{
        /*                   : unregister_chrdev_regio(dev_t from, unsigned count)
         * @description      : 释放设备号
         * @param – from     : 需要释放的设备号。
         * @param - count    : 表示从 from 开始,要释放的设备号数量。
         * @return           : 无
         */
        unregister_chrdev_region(xxx_dev_no, 1); // 释放设备号
        cdev_del(&xxx_dev.cdev);                 // 注销cdev设备
}

register_chrdev()与register_chrdev_region()

        register_chrdev()函数不适用cdev结构体,老版本一直使用此函数,而2.6内核才出现了cdev结构体。

/*                   : register_chrdev
 * @description      : 字符设备注册函数,一般放在驱动模块的入口函数modlue_init
 * @param – major    : 主设备号,Linux 下每个设备都有一个主设备号和次设备号
 * @param - name     : 设备名字,指向一串字符串。即调用注册函数生成/dev/name
 * @param - fops     : 结构体 file_operations 类型指针,指向设备的操作函数集合变量          
 * @return           : 0 成功;其他 失败
 */
static inline int register_chrdev(unsigned int major, const char *name,
                                    const struct file_operations *fops)

/*                   : unregister_chrdev
 * @description      : 字符设备注销函数,一般放在驱动模块的出口函数modlue_exit
 * @param – major    : 主设备号,Linux 下每个设备都有一个主设备号和次设备号
 * @param - name     : 设备名字,指向一串字符串。         
 * @return           : 无
 */
static inline void unregister_chrdev(unsigned int major, const char *name)

        调用register_chrdev()不能指定次设备号,那么函数内部会在内核中连续注册256个次设备号,也就是所有的次设备号都会被占用,而在大多数情况下都不会用到这么多次设备号,所以会造成极大的资源浪费。

源码解析:register_chrdev深入分析 - 程序员大本营 (pianshen.com)

1.2.2 file_operations中的成员函数

/*               : xxx_read
 * @description  : 从设备读文件,对应read函数
 * @param – filp : 要打开的设备文件(文件描述符)
 * @param – buf  : 返回给用户空间的数据缓冲区
 * @param – cnt  : 要读取的数据长度
 * @param – offt : 相对于文件首地址的偏移
 * @return       : 读取的字节数,如果为负值,表示读取失败
 */
ssize_t xxx_read(struct file *filp, char __user *buf,
								size_t cnt, loff_t *offt)
{
	......
    copy_to_usr(buf, ..., ...);
    ......
}

/*                   :xxx_write
 * @description      : 向设备写数据,对应write函数
 * @param - pfilp_t  : 设备文件,表示打开的文件描述符
 * @param - pusr_buf : 要写给设备写入的数据
 * @param - cnt      : 要写入的数据长度
 * @param - pofft_t  : 相对于文件首地址的偏移
 * @return           : 写入的字节数,如果为负值,表示写入失败
 */
ssize_t xxx_write(struct file *pfilp_t,const char __user *pusr_buf,
				   size_t cnt, loff_t *pofft_t)
{
    ......
    copy_from_usr(..., buf, ...);
    ......
}

/*                   :xxx_open
 * @description      : 打开设备,对应open函数
 * @param – inode    : 传递给驱动的 inode
 * @param - pfilp_t  : 设备文件, file 结构体有个叫做 private_data 的成员变量
 *                   一般在 open 的时候将 private_data 指向设备结构体。
 * @return           : 0 成功;其他 失败
 */
int xxx_open(struct inode *pinode_t, struct file *pfilp_t)
{
	......
	return 0;
}

/*                  :xxx_release
 * @description     : 关闭/释放设备,对应close函数
 * @param - pfilp_t : 要关闭的设备文件(文件描述符)
 * @return          : 0 成功;其他 失败
 */
int xxx_release(struct inode *pinode_t, struct file *pfilp_t)
{
    // 用户实现具体功能
	return 0;
}

/*                   : xxx_ioctl
 * @description      : 驱动实现ioctl,获取用户态发送的cmd
 * @param – pfilp_t  : 设备文件
 * @param - cmd      : 用户态发送给内核的cmd
 * @param - arg      : 用户态发给内核的额外的参数
 * @return           : 0 成功;其他 失败
 */
long xxx_ioctl(struct file *pfilp_t, unsigned int cmd, unsigned long arg)
{
	switch(cmd)
	{
		case xxx:
		{
			......
			break;
		}
		
		case xxx:
		{
			......
			break;
		}
		
		default:
		{
			......
			return -ENOTTY;
		}
	}
	return 0;
}

static struct file_operations xxx_fops = {
	.owner = THIS_MODULE,
	.unlocked_ioctl = xxx_ioctl,          //指定驱动函数对应系统调用的ioctl
	.open = xxx_open,                     //指定驱动函数对应系统调用的open
	.release = xxx_release,               //指定驱动函数对应系统调用的close
	.write = xxx_write,                   //指定驱动函数对应系统调用的write
	.read = xxx_read,                     //指定驱动函数对应系统调用的read
};

1.2.3 用户空间与内核空间的内存复制

        用户空间不能直接访问内核的内存,因此需要借助copy_to_usr()和copy_from_usr()函数。

/*                   : copy_to_user
 * @description      : 内核空间将数据拷贝到用户空间的“buf”
 * @param – to       : 目标地址,这个地址是用户空间接收数据的地址,即buf;
 * @param - from     : 源地址,为内核空间需要发送的数据的地址
 * @param - count    : 拷贝的字节数
 * @return           : 0 成功;负值 失败
 */
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
// 即copy_to_user(buf, const void *from, unsigned long count)

/*                   : copy_from_user
 * @description      : 用户空间的“buf”拷贝到内核空间
 * @param – to       : 目标地址,这个地址是内核空间接收数据的地址
 * @param - from     : 源地址,为用户空间需要发送的数据的地址,“buf”
 * @param - count    : 拷贝的字节数
 * @return           : 0 成功;负值 失败
 */
unsigned long copy_from_user(void *to, void __user *from, unsigned long count);
// 即copy_to_user(void *to, buf, unsigned long count)

__user是一个宏,表明其后的指针指向用户空间/include/linux/compiler.h中有如下定义:

#ifdef __CHECKER__
# define __user     __attribute__((noderef, address_space(1)))
#else
# define __user
#endif

        如果内核空间与用户空间只传递简单的值(int、long、char等),可以使用更简单的get_user()put_user()函数。

int var; // 内核空间整型变量

get_user(var, (int *)arg); // 用户--->内核,arg是用户空间地址

put_user(val, (int *)arg); // 内核--->用户,arg是用户空间地址

        无论是copy_to_user()/copy_from_user()还是get_user()/put_user()在函数内部都会执行一个合法性检测。具体就是:内核虽然可以访问用户空间的缓冲区,但在访问之前会检查其合法性,通过access_ok(type, addr, size)函数进行判断,来确定待访问的空间是否属于用户空间

        以copy_to_user函数源码举例:

unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)

{

       might_sleep();

       BUG_ON((long) n < 0);

       if (access_ok(VERIFY_WRITE, to, n))

              n = __copy_to_user(to, from, n);

       return n;

}
EXPORT_SYMBOL(copy_to_user);

        如果使用__put_user__get_user函数不会检测用户空间的合法性,需要自己进行合法性检测,这两者与put_user和get_user的区别就是后两者会自动进行合法性检测,而前者不会。

1.3 字符设备驱动的结构

Linux字符设备驱动_第1张图片

1.4 字符设备节点创建

        在完成上述 1.2 的字符设备驱动组成后,将写好的模块.c进行编译,生成.ko文件,.ko文件移植到开发板后,再执行相关命令:

insmod xxx.ko // 加载模块

lsmod         // 查看当前系统已挂载的模块

mkmod /dev/xxx c major minor // 创建设备节点,c表示字符设备,major主设备号,minor次设备号

rmmod xxx.ko  // 卸载模块

你可能感兴趣的:(嵌入式,c语言,驱动开发,linux)