字元裝置是Linux系統中常見的裝置類型,它們以位元組為單位進行資料傳輸,如鍵盤,滑鼠,串列埠等。字元設備的驅動是Linux驅動程式開發中最基本的一種,它涉及到設備號,設備文件,文件操作結構體,cdev結構體等概念。在本文中,我們將介紹Linux字元裝置驅動框架的原理和方法,包括註冊和註銷設備號,創建和刪除設備文件,初始化和釋放cdev結構體,實現和註冊文件操作函數等,並舉例說明它們的使用方法和注意事項。
Linux一切皆文件,那麼作為一個設備文件,它的操作方法接口封裝在struct file_operations
,當我們寫一個驅動的時候,一定要實現相應的接口,這樣才能使這個驅動可用,Linux的核心中大量使用」註冊回調」機制進行驅動程式的編寫,所謂註冊回調,簡單的理解,就是當我們open一個設備文件的時候,其實是透過VFS找到相應的inode,並執行此前創建這個設備文件時註冊在inode中的open函數,其他函數也是如此,所以,為了讓我們寫的驅動能夠正常的被應用程式操作,首先要做的就是實現相應的方法,然後再創建相應的設備文件。
#include //for struct cdev #include //for struct file #include //for copy_to_user #include //for error number static int ma = 0; static int mi = 0; const int count = 3; /* 准备操作方法集 */ /* struct file_operations { struct module *owner; //THIS_MODULE //读设备 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); //写设备 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //映射内核空间到用户空间 int (*mmap) (struct file *, struct vm_area_struct *); //读写设备参数、读设备状态、控制设备 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //打开设备 int (*open) (struct inode *, struct file *); //关闭设备 int (*release) (struct inode *, struct file *); //刷新设备 int (*flush) (struct file *, fl_owner_t id); //文件定位 loff_t (*llseek) (struct file *, loff_t, int); //异步通知 int (*fasync) (int, struct file *, int); //POLL机制 unsigned int (*poll) (struct file *, struct poll_table_struct *); 。。。 }; */ ssize_t myread(struct file *filep, char __user * user_buf, size_t size, loff_t* offset) { return 0; } struct file fops = { .owner = THIS_MODULE, .read = myread, ... }; /* 字符设备对象类型 struct cdev { struct kobject kobj; struct module *owner; //模块所有者(THIS_MODULE),用于模块计数 const struct file_operations *ops; //操作方法集(分工:打开、关闭、读/写、...) struct list_head list; dev_t dev; //设备号(第一个) unsigned int count; //设备数量 }; */ static int __init chrdev_init(void) { ... /* 构造cdev设备对象 */ struct cdev *cdev_alloc(void); /* 初始化cdev设备对象 */ void cdev_init(struct cdev*, const struct file_opeartions*); /* 申请设备号,静态or动态*/ /* 为字符设备静态申请第一个设备号 */ 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); ma = MAJOR(dev) //从dev_t数据中得到主设备号 mi = MINOR(dev) //从dev_t数据中得到次设备号 MKDEV(ma,1) //将主设备号和次设备号组合成设备号,多用于批量创建/删除设备文件 /* 注册字符设备对象cdev到内核 */ int cdev_add(struct cdev* , dev_t, unsigned); ... } static void __exit chrdev_exit(void) { ... /* cdev_del()、cdev_put()二选一 */ /* 从内核注销cdev设备对象 */ void cdev_del(struct cdev* ); /* 从内核注销cdev设备对象 */ void cdev_put(stuct cdev *); /* 回收设备号 */ void unregister_chrdev_region(dev_t from, unsigned count); ... }
囉嗦一句,如果使用靜態申請設備號,那麼最大的問題就是不要與已知的設備號相衝突,內核在文檔**”Documentation/devices.txt”**中已經註明了哪些主設備號碼被使用了,從中可以看出,在2^12個主設備號中,我們能夠使用的範圍是240-255以及261-2^12-1的部分,這也可以解釋為什麼我們動態申請的時候,設備號經常是250的原因。此外,透過這個文件,我們也可以看出,”主設備號表徵一類設備”,但是字元/塊設備本身就可以被分為好多類,所以內核給他們每一類都分配了主設備號。
#Linux下各個進程都有自己獨立的進程空間,即使是將核心的資料映射到用戶進程,該資料的PID也會自動轉變為該用戶進程的PID,由於這種機制的存在,我們不能直接將資料從核心空間和使用者空間進行拷貝,而需要專門的拷貝資料函數/巨集:
long copy_from_user(void *to, const void __user * from, unsigned long n) long copy_to_user(void __user *to, const void *from, unsigned long n)
這兩個函數可以將核心空間的資料拷貝到回呼該函數的使用者行程的使用者行程空間,有了這兩個函數,核心中的read,write就可以實現核心空間和使用者空間的資料拷貝。
ssize_t myread(struct file *filep, char __user * user_buf, size_t size, loff_t* offset) { long ret = 0; size = size > MAX_KBUF?MAX_KBUF:size; if(copy_to_user(user_buf, kbuf,size) return -EAGAIN; } return 0; }
#ioctl是Linux專門為用戶層控制設備設計的系統呼叫接口,這個接口具有極大的靈活性,我們的設備打算讓用戶通過哪些命令實現哪些功能,都可以通過它來實現,ioctl在操作方法集中對應的函數指標是long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
,其中的指令和參數完全由驅動指定,通常指令會寫在一個頭檔中以供應用層和驅動層遵守同樣的通訊協議,Linux建議如圖所示的方式定義ioctl()指令
设备类型 序列号 方向 数据尺寸 8bit 8bit 2bit 13/14bit
“
设备类型字段为一个幻数,可以是0~0xff之间的数,内核中的”ioctl-number.txt“给出了一个推荐的和已经被使用的幻数(但是已经好久没人维护了),新设备驱动定义幻数的时候要避免与其冲突。
序列号字段表示当前命令是整个ioctl命令中的第几个,从1开始计数。
方向字段为2bit,表示数据的传输方向,可能的值是:**_IOC_NONE,_IOC_READ,_IOC_WRITE和_IOC_READ|_IOC_WRITE**。
数据尺寸字段表示涉及的用户数据的大小,这个成员的宽度依赖于体系结构,通常是13或14位。”
内核还定义了**_IO(),_IOR(),_IOW(),_IOWR()**这4个宏来辅助生成这种格式的命令。这几个宏的作用是根据传入的type(设备类型字段),nr(序列号字段)和size(数据长度字段)和方向字段移位组合生成命令码。
内核中还预定义了一些I/O控制命令,如果某设备驱动中包含了与预定义命令一样的命令码,这些命令会被当做预定义命令被内核处理而不是被设备驱动处理,有如下4种:
//mycmd.h ... #include #define CMDT 'A' #define KARG_SIZE 36 struct karg{ int kval; char kbuf[KARG_SIZE]; }; #define CMD_OFF _IO(CMDT,0) #define CMD_ON _IO(CMDT,1) #define CMD_R _IOR(CMDT,2,struct karg) #define CMD_W _IOW(CMDT,3,struct karg) ... //chrdev.c static long demo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { static struct karg karg = { .kval = 0, .kbuf = {0}, }; struct karg *usr_arg; switch(cmd){ case CMD_ON: /* 开灯 */ break; case CMD_OFF: /* 关灯 */ break; case CMD_R: if(_IOC_SIZE(cmd) != sizeof(karg)){ return -EINVAL; } usr_arg = (struct karg *)arg; if(copy_to_user(usr_arg, &karg, sizeof(karg))){ return -EAGAIN; } break; case CMD_W: if(_IOC_SIZE(cmd) != sizeof(karg)){ return -EINVAL; } usr_arg = (struct karg *)arg; if(copy_from_user(&karg, usr_arg, sizeof(karg))){ return -EAGAIN; } break; default: ; }; return 0; }
插入的设备模块,我们就可以使用cat /proc/devices命令查看当前系统注册的设备,但是我们还没有创建相应的设备文件,用户也就不能通过文件访问这个设备。设备文件的inode应该是包含了这个设备的设备号,操作方法集指针等信息,这样我们就可以通过设备文件找到相应的inode进而访问设备。创建设备文件的方法有两种,手动创建或自动创建,手动创建设备文件就是使用mknod /dev/xxx 设备类型 主设备号 次设备号的命令创建,所以首先需要使用cat /proc/devices查看设备的主设备号并通过源码找到设备的次设备号,需要注意的是,理论上设备文件可以放置在任何文件加夹,但是放到**”/dev”才符合Linux的设备管理机制,这里面的devtmpfs是专门设计用来管理设备文件的文件系统。设备文件创建好之后就会和创建时指定的设备绑定,即使设备已经被卸载了,如要删除设备文件,只需要像删除普通文件一样rm**即可。理论上模块名(lsmod),设备名(/proc/devices),设备文件名(/dev)并没有什么关系,完全可以不一样,但是原则上还是建议将三者进行统一,便于管理。
除了使用蹩脚的手动创建设备节点的方式,我们还可以在设备源码中使用相应的措施使设备一旦被加载就自动创建设备文件,自动创建设备文件需要我们在编译内核的时候或制作根文件系统的时候就好相应的配置:
Device Drivers ---> Generic Driver Options ---> [*]Maintain a devtmpfs filesystem to mount at /dev [*] Automount devtmpfs at /dev,after the kernel mounted the rootfs
OR
制作根文件系统的启动脚本写入
mount -t sysfs none sysfs /sys mdev -s //udev也行
有了这些准备,只需要导出相应的设备信息到**”/sys”**就可以按照我们的要求自动创建设备文件。内核给我们提供了相关的API
class_create(owner,name); struct device *device_create_vargs(struct class *cls, struct device *parent,dev_t devt, void *drvdata,const char *fmt, va_list vargs); void class_destroy(struct class *cls); void device_destroy(struct class *cls, dev_t devt);
有了这几个函数,我们就可以在设备的**xxx_init()和xxx_exit()**中分别填写以下的代码就可以实现自动的创建删除设备文件
/* 在/sys中导出设备类信息 */ cls = class_create(THIS_MODULE,DEV_NAME); /* 在cls指向的类中创建一组(个)设备文件 */ for(i= minor;i"%s%d",DEV_NAME,i); } /* 在cls指向的类中删除一组(个)设备文件 */ for(i= minor;i
通过本文,我们了解了Linux字符设备驱动框架的原理和方法,它们可以用来实现对字符设备的驱动和控制。我们应该根据实际需求选择合适的方法,并遵循一些基本原则,如使用静态或动态分配的设备号,使用标准或自定义的设备文件名,使用正确或简化的cdev初始化方式等。字符设备驱动框架是Linux驱动开发中最重要的一部分,它可以实现对字符设备的读写操作,也可以提升驱动程序的可移植性和可维护性。希望本文能够对你有所帮助和启发。
以上是Linux字元裝置驅動框架:原理與方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!