Linux字符设备驱动
-
字符设备是指在I/O传输过程中以字符为单位进行传输的设备,例如键盘,打印机等。在UNIX系统中,字符设备以特别文件方式在文件目录树中占据位置并拥有相应的结点。
-
字符设备可以使用与普通文件相同的文件操作命令对字符设备文件进行操作,例如打开、关闭、读、写等。
-
当一台字符型设备在硬件上与主机相连之后,必须为这台设备创建字符特别文件。操作系统的mknod命令被用来建立设备特别文件。例如为一台终端创建名为/dev/tty03的命令如下(设主设备号为2,次设备为13,字符型类型标记c):
mknod /dev/tty03 c 2 13
- 此后,open, close, read, write等系统调用适用于设备文件/dev/tty03。
- 设备与驱动程序的通信方式依赖于硬件接口。当设备上的数据传输完成时,硬件通过总线发出中断信号导致系统执行一个中断处理程序。中断处理程序与设备驱动程序协同工作完成数据传输的底层控制。
Linux字符设备驱动结构
1、结构体定义
struct cdev {
struct kobject kobj; /* 内嵌的 kobject 对象*/
struct module *owner; /* 所属模块 */
struct file_operations *ops; /* 文件操作结构体 */
struct list_head list;
dev_t dev; /* 设备号 */
unsigned int count;
};
cdev结构体的dev_t成员定义了设备号,为32位,其中12位为主设备号,20位为次设备号。使用下列宏可以从dev_t获得主设备号和次设备号,以及生成新的设备号:
MAJOR(dev_t dev)
MINOR(dev_t dev)
MKDEV(int major, int minor)
2、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);
file_operations结构体
file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行Linux的open()、write()、read()、close()等系统调用时最终被内核调用。file_operations结构体目前已经比较庞大,它的定义如下。
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 (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
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 *, loff_t, loff_t, 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 **);
long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
- llseek()函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
- read()函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。它与用户空间应用程序中的
ssize_t read(int fd,void*buf,size_t count)
和size_t fread(void*ptr,size_t size,size_t nmemb,FILE*stream)
对应。 - write()函数向设备发送数据,成功时该函数返回写入的字节数。如果此函数未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值。它与用户空间应用程序中的
ssize_t write(int fd,const void*buf,size_t count)
和size_t fwrite(const void*ptr,size_t size,size_t nmemb,FILE*stream)
对应。 - read()和write()如果返回0,则暗示end-of-file(EOF)。unlocked_ioctl()提供设备相关控制命令的实现(既不是读操作,也不是写操作),当调用成功时,返回给调用程序一个非负值。它与用户空间应用程序调用的
int fcntl(int fd,int cmd,.../*arg*/)
和int ioctl(int d,int request,...)
对应。 - mmap()函数将设备内存映射到进程的虚拟地址空间中,如果设备驱动未实现此函数,用户进行mmap()系统调用时将获得-ENODEV返回值。这个函数对于帧缓冲等设备特别有意义,帧缓冲被映射到用户空间后,应用程序可以直接访问它而无须在内核和应用间进行内存复制。它与用户空间应用程序中的
void*mmap(void*addr,size_t length,int prot,int flags,int fd,off_t offset)
函数对应。 - 当用户空间调用Linux API函数open()打开设备文件时,设备驱动的open()函数最终被调用。驱动程序可以不实现这个函数,在这种情况下,设备的打开操作永远成功。与open()函数对应的是release()函数。
- poll()函数一般用于询问设备是否可被非阻塞地立即读写。当询问的条件未触发时,用户空间进行select()和poll()系统调用将引起进程的阻塞。
- aio_read()和aio_write()函数分别对与文件描述符对应的设备进行异步读、写操作。设备实现这两个函数后,用户空间可以对该设备文件描述符执行SYS_io_setup、SYS_io_submit、SYS_io_getevents、SYS_io_destroy等系统调用进行读写。
Linux字符设备驱动的组成
1、字符设备驱动模块加载和卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请和cdev的注册,而在卸载函数中应实现设备号的释放和cdev的注销。Linux内核的编码习惯是为设备定义一个设备相关的结构体,该结构体包含设备所涉及的cdev、私有数据及锁等信息。常见的设备结构体、模块加载和卸载函数形式如下。
/* 设备结构体 */
struct xxx_dev_t {
struct cdev cdev;
...
} xxx_dev;
/* 设备驱动模块加载函数 */
static int _ _init xxx_init(void)
{
...
cdev_init(&xxx_dev.cdev, &xxx_fops); /* 初始化 cdev */
xxx_dev.cdev.owner = THIS_MODULE;
/* 获取字符设备号 */
if (xxx_major) {
register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
} else {
alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
}
ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注册设备 */
...
}
/* 设备驱动模块卸载函数 */
static void _ _exit xxx_exit(void)
{
unregister_chrdev_region(xxx_dev_no, 1); /* 释放占用的设备号 */
cdev_del(&xxx_dev.cdev); /* 注销设备 */
...
}
2、字符设备驱动的file_operations结构体中的成员函数
file_operations结构体中的成员函数是字符设备驱动与内核虚拟文件系统的接口,是用户空间对Linux进行系统调用最终的落实者。大多数字符设备驱动会实现read()、write()和ioctl()函数,常见的字符设备驱动的这3个函数的形式如下:
/* 读设备 */
ssize_t xxx_read(struct file *filp, char __user *buf, size_t count, loff_t*f_pos)
{
...
copy_to_user(buf, ..., ...);
...
}
/********************************示例读函数************************************/
static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size, loff_t * ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data;
if (p >= GLOBALMEM_SIZE) /* 读偏移量大于内存空间阈值,返回读失败 */
return 0;
if (count > GLOBALMEM_SIZE - p) /* 读取量大于阈值与读偏移量之差,仅读出至开头至读偏移量区间的数据 */
count = GLOBALMEM_SIZE - p;
if (copy_to_user(buf, dev->mem + p, count)) {
ret = -EFAULT;
} else {
*ppos += count;
ret = count;
printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
}
return ret;
}
/*********************************************************************************/
/* 写设备 */
ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
...
copy_from_user(..., buf, ...);
...
}
/********************************示例写函数************************************/
static ssize_t globalmem_write(struct file *filp, const char __user * buf, size_t size, loff_t * ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data;
if (p >= GLOBALMEM_SIZE)
return 0;
if (count > GLOBALMEM_SIZE - p)
count = GLOBALMEM_SIZE - p;
if (copy_from_user(dev->mem + p, buf, count))
ret = -EFAULT;
else {
*ppos += count;
ret = count;
printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
}
return ret;
}
/*********************************************************************************/
/* ioctl 函数 */
long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
...
switch (cmd) {
case XXX_CMD1:
...
break;
case XXX_CMD2:
...
break;
default:
/* 不能支持的命令 */
return - ENOTTY;
}
return 0;
}
/********************************示例ioctl函数************************************/
#define GLOBALMEM_MAGIC 'g'
#define MEM_CLEAR _IO(GLOBALMEM_MAGIC,0)
static long globalmem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct globalmem_dev *dev = filp->private_data;
switch (cmd) {
case MEM_CLEAR:
memset(dev->mem, 0, GLOBALMEM_SIZE);
printk(KERN_INFO "globalmem is set to zero\n");
break;
default:
return -EINVAL;
}
return 0;
}
/*********************************************************************************/
/********************************示例seek函数************************************/
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
loff_t ret = 0;
switch (orig) {
case 0: /* 从文件开头位置 seek */
if (offset< 0) {
ret = -EINVAL;
break;
}
if ((unsigned int)offset > GLOBALMEM_SIZE) {
ret = -EINVAL;
break;
}
filp->f_pos = (unsigned int)offset;
ret = filp->f_pos;
break;
case 1: /* 从文件当前位置开始 seek */
if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
ret = -EINVAL;
break;
}
if ((filp->f_pos + offset) < 0) {
ret = -EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = -EINVAL;
break;
}
return ret;
}
/*********************************************************************************/
-
在上述程序中,MEM_CLEAR被宏定义为0x01,实际上这并不是一种值得推荐的方法,简单地对命令定义为0x0、0x1、0x2等类似值会导致不同的设备驱动拥有相同的命令号。如果设备A、B都支持0x0、0x1、0x2这样的命令,就会造成命令码的污染。因此,Linux内核推荐采用一套统一的ioctl()命令生成方式
- Linux建议以如图所示的方式定义ioctl()的命令
-
命令码的设备类型字段为一个“幻数”,可以是0~0xff的值,内核中的ioctl-number.txt给出了一些推荐的和已经被使用的“幻数”,新设备驱动定义“幻数”的时候要避免与其冲突。
-
命令码的序列号也是8位宽。
-
命令码的方向字段为2位,该字段表示数据传送的方向,可能的值是
_IOC_NONE
(无数据传输)、_IOC_READ
(读)、_IOC_WRITE
(写)和_IOC_READ
|_IOC_WRITE
(双向)。数据传送的方向是从应用程序的角度来看的。内核还定义了_IO()
、_IOR()
、_IOW()
和_IOWR()
这4个宏来辅助生成命令#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) #define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),\ (_IOC_TYPECHECK(size))) #define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),\ (_IOC_TYPECHECK(size))) #define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr), (_IOC_TYPECHECK(size))) /* _IO 、 _IOR 等使用的 _IOC 宏 */ #define _IOC(dir,type,nr,size) \ (((dir) << _IOC_DIRSHIFT) | \ ((type) << _IOC_TYPESHIFT) | \ ((nr) << _IOC_NRSHIFT) | \ ((size) << _IOC_SIZESHIFT))
-
命令码的数据长度字段表示涉及的用户数据的大小,这个成员的宽度依赖于体系结构,通常是13或者14位。
-
由于用户空间不能直接访问内核空间的内存,因此借助了函数copy_from_user()完成用户空间缓冲区到内核空间的复制,以及copy_to_user()完成内核空间到用户空间缓冲区的复制,见代码第6行和第14行。
-
如果要复制的内存是简单类型,如char、int、long等,则可以使用简单的put_user()和get_user(),如
int val; ... get_user(val, (int *) arg); /* 内核空间整型变量 /* 用户→内核, arg 是用户空间的地址 */ ... put_user(val, (int *) arg); /* 内核→用户, arg 是用户空间的地址 */
-
内核空间虽然可以访问用户空间的缓冲区,但是在访问之前,一般需要先检查其合法性,通过access_ok(type,addr,size)进行判断,以确定传入的缓冲区的确属于用户空间,例如:
static ssize_t read_port(struct file *file, char __user *buf,size_t count, loff_t *ppos) { unsigned long i = *ppos; char __user *tmp = buf; if (!access_ok(VERIFY_WRITE, buf, count)) return -EFAULT; while (count-- > 0 && i < 65536) { if (__put_user(inb(i), tmp) < 0) return -EFAULT; i++; tmp++; } *ppos = i; return tmp-buf; }
-
在内核空间与用户空间的界面处,内核检查用户空间缓冲区的合法性显得尤其必要,Linux内核的许多安全漏洞都是因为遗忘了这一检查造成的,copy_to_user()与copy_from_user()均实现了用户空间缓冲区的合法性校验。
字符设备驱动的结构、字符设备驱动与字符设备以及字符设备驱动与用户空间访问该设备的程序之间的关系图如下: