千家信息网

Linux内核设备驱动之字符设备驱动的示例分析

发表于:2026-11-02 作者:千家信息网编辑
千家信息网最后更新 2026年11月02日,小编给大家分享一下Linux内核设备驱动之字符设备驱动的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!/******************** * 字符设备驱动 *****
千家信息网最后更新 2026年11月02日Linux内核设备驱动之字符设备驱动的示例分析

小编给大家分享一下Linux内核设备驱动之字符设备驱动的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!

/******************** * 字符设备驱动 ********************/

(1)字符设备驱动介绍

字符设备是指那些按字节流访问的设备,针对字符设备的驱动称为字符设备驱动。

此类驱动适合于大多数简单的硬件设备。比如并口打印机,我们通过在/dev下建立一个设备文件(如/dev/printer)来访问它。

用户应用程序用标准的open函数打开dev/printer,然后用write向文件中写入数据,用read从里面读数据。

调用流程:

  • write(): 用户空间 -->

  • sys_write(): VFS -->

  • f_op->write: 特定设备的写方法

所谓驱动,就是提供最后的write函数,通过访问打印机硬件的寄存器直接和打印机对话

(2)主设备号和次设备号

a.设备编号介绍

对字符设备的访问是通过文件系统内的设备文件进行的。这些文件位于/dev。用"ls -l"查看。

设备通过设备号来标识。设备号分两部分,主设备号和次设备号。

通常,主设备号标示设备对应的驱动程序,linux允许多个驱动共用一个主设备号;

而次设备号用于确定设备文件所指的设备。

在内核中,用dev_t类型保存设备编号。

2.4内核中采用16位设备号(8位主,8位从),而2.6采用32位,12位主,20位从。

在驱动中访问设备号应该用中定义的宏。

获取设备号:

  • MAJOR(dev_t dev)

  • MINOR(dev_t dev)

  • MKDEV(int major, int minor)

b.分配和释放设备编号

在建立一个字符设备前,驱动需要先获得设备编号。

分配:

#include int register_chrdev_region(dev_t first, unsigned int count, char *name);//first:要分配的设备编号范围的起始值(次设备号常设为0)//count: 所请求的连续编号范围//name: 和编号关联的设备名称(见/proc/devices)

也可以要求内核动态分配:

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);//firstminor: 通常为0//*dev: 存放内核返回的设备号

释放:

void unregister_chrdev_region(dev_t first, unsigned int count);//在模块的清除函数中调用

在Documentation/devices.txt中可以找到内核已经分配的设备号。

c.建立设备文件

当设备驱动模块向系统申请了主设备号和次设备号,并且已经通过insmod加载到内核中后,我们就可以通过在/dev下创建设备文件来访问这个设备了。

字符设备的创建:$>mknod /dev/mychar c major minor

我们在驱动中常常采用动态分配主次设备号的方法,这样不会和系统中已有的设备号冲突。

动态分配时,/dev下的设备文件也需要通过分析/proc/devices动态建立。

见char_load和char_unload脚本。

(3)字符设备的基本数据结构

和字符设备驱动关系最紧密的3个基本的数据结构是:file, file_oepeations和inode

a.file_operations数据结构

结构中包含了若干函数指针。这些函数就是实际和硬件打交道的函数。

用户空间调用的open,write等函数最终会调用这里面的指针所指向的函数。每个打开的文件和一组函数关联。

见和驱动书的p54

2.6内核结构的初始化:

struct file_operations my_fops = {.owner = THIS_MODULE,.llseek = my_llseek,.read = my_read,.write = my_write,.ioctl = my_ioctl,.open = my_open,.release = my_release,}

2.4内核结构的初始化:

struct file_operations my_fops = {owner: THIS_MODULE,llseek: my_llseek,...}

b.file结构

file是一个内核结构体,实际上和用户open文件后返回的文件描述符fd对应。

file结构代表一个打开的文件,系统中每个打开的文件在内核空间都有一个对应的file结构。

它由内核在open时创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数,在文件的所有实例都被关闭后,内核会释放这个结构。

用户空间进程fork一个新进程后,新老进程会共享打开的文件描述符fd,这个操作不会在内核空间创建新的file结构,只会增加已创建file结构的计数。

见

mode_t f_mode; 通过FMODE_READ和FMODE_WRITE标示文件是否可读或可写。

loff_t f_pos; 当前的读写位置,loff_t为64位

unsigned int f_flags; 文件标志,如O_RDONLY, O_NONBLOCK, O_SYNC。标志都定义在

struct file_operations *f_op; 与文件相关的操作。内核在执行open时对这个指针赋值。可以在驱动的open方法中根据次设备号赋予不同的f_op

void *private; 通常将表示硬件设备的结构体赋给private.

struct dentry *f_dentry; 文件对应的目录项(dentry)结构。可通过filp->f_dentry->d_inode访问索引节点。

file中其他的内容和驱动关系不大。

c.inode结构

内核用inode结构表示一个实际的文件,可以是一个普通的文件,也可以是一个设备文件。

每个文件只有一个inode结构,而和文件描述符对应的file结构可以有多个(多次进行open调用)。这些file都指向同一个inode。

inode定义在

dev_t i_rdev; 对于表示设备文件的inode结构,i_rdev里包含了真正的设备编号

struct cdev *i_cdev cdev是表示字符设备的内核的内部结构。当inode表示一个字符设备时,i_cdev指向内核中的struct cdev.

其他结构和设备驱动关系不大。

用如下宏从inode获取设备号:

  • unsigned int iminor(struct inode *inode)

  • unsigned int imajor(struct inode *inode)

(4)字符设备的注册

内核内部使用struct cdev结构来表示一个字符设备。

我们的驱动要把自己的cdev注册到内核中去。见

a.通常在设备的结构中加入cdev

struct scull_dev{...struct cdev cdev; /* 字符设备结构 */}

b.初始化

void cdev_init(struct cdev *cdev, struct file_operations *fops)

c.设定cdev中的内容

  • dev->cdev.owner = THIS_MODULE;

  • dev->cdev.ops = &scull_fops;

d.向内核添加设定好的cdev

int cdev_add(struct cdev *dev, dev_t num, unsigned int count);//num: 设备对应的第一个编号//count: 和设备关联的设备编号的数量,常取1//一旦cdev_add返回,内核就认为设备可以使用了,所以要在调用之前完成设备的硬件初始化。

(5)老式的注册函数

2.4中的老式注册函数仍然在驱动函数中大量存在,但新的代码不应该使用这些代码。

注册:

int register_chrdev(unsigned int major,  const char *name,  struct file_operations *fops);//为给定的主设备号注册0~255作为次设备号,并为每个设备建立一个对应的默认cdev结构

注销:

int unregister_chrdev(unsigned int major,  const char *name);

(6)open和release

a.open

在驱动的open方法中完成设备的初始化工作,open完成后,硬件就可以使用,用户程序可以通过write等访问设备,open的工作有:

  • *检查设备的特定错误

  • *如果设备首次打开,则对其进行初始化(有可能多次调用open)

  • *如有必要,更新f_op指针

  • *分配并填写置于filp->private_data中的数据

open原型;

int (*open) (struct inode *inode, struct file *filp);//在open中通过inode获得dev指针,并将其赋给file->private_data//struct scull_dev *dev;//dev = contain_of(inode->i_cdev, struct scull_dev, cdev);//filp->private_data = dev;//(如果dev是静态分配的,则在open或write等方法中可以直接访问dev,但如果dev是在module_init时动态分配的,则只能通过上面的方法获得其指针)

b.release

并不是每个close调用都会引起对release方法的调用,只有当file的计数器归零时,才会调用release,从而释放dev结构)

(7)read和write

read和write的工作是从用户空间拷贝数据到内核,或是将内核数据拷贝到用户空间。其原型为:

ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);//buff: 用户空间的缓冲区指针//offp: 用户在文件中进行存取操作的位置//在read和write中,拷贝完数据后,应该更新offp,并将实际完成的拷贝字节数返回。

(8)和用户空间交换数据

read和write中的__user *buff 是用户空间的指针,内核不能直接引用其中的内容(也就是不能直接对buff进行取值操作),需要通过内核提供的函数进行数据拷贝。其原因是:

  • a.在不同架构下,在内核模式中运行时,用户空间的指针可能是无效的。

  • b.用户空间的内存是分页的,系统调用执行时,buff指向的内存可能根本不在RAM中(被交换到磁盘中了)

  • c.这可能是个无效或者恶意指针(比如指向内核空间)

内核和用户空间交换数据的函数见

如:

1. unsigned long copy_to_user(
void __user *to,
const void *from,
unsigned long count);
//向用户空间拷贝数据

2. unsigned long copy_from_user(
void *to,
const void __user *from,
unsigned long count);
//从用户空间获得数据

3. int put_user(datum, ptr)
//向用户空间拷贝数据。字节数由sizeof(*ptr)决定
//返回值为0成功,为负错误。

4. int get_user(local, ptr);
//从用户空间获得数据。字节数由sizeof(*ptr)决定
//返回值和local都是从用户空间获得的数据

任何访问用户空间的函数都必须是可睡眠的,这些函数需要可重入。

copy_to_user等函数如果返回值不等于0,则read或write应向用户空间返回-EFAULT

主设备号用来表示设备驱动, 次设备号表示使用该驱动的设备

在内核dev_t 表示设备号, 设备号由主设备号和次设备号组成

#include #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //根据设备号获取主设备号#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //获取次设备号#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))     //根据指定的主设备和次设备号生成设备号#include  //静态:申请指定的设备号, from指设备号, count指使用该驱动有多少个设备(次设备号), 设备名 int register_chrdev_region(dev_t from, unsigned count, const char *name);//name的长度不能超过64字节 //动态申请设备号, 由内核分配没有使用的主设备号, 分配好的设备存在dev, baseminor指次设备号从多少开始, count指设备数, name设备名 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)//释放设备号, from指设备号, count指设备数void unregister_chrdev_region(dev_t from, unsigned count)//cat /proc/devices 可查看设备使用情况在内核源码的documentations/devices.txt可查看设备号的静态分配情况///内核里使用struct cdev来描述一个字符设备驱动 #include struct cdev {struct kobject kobj;    //内核用于管理字符设备驱动 struct module *owner;   //通常设为THIS_MODULE, 用于防止驱动在使用中时卸载驱动模块const struct file_operations *ops; //怎样操作(vfs)struct list_head list;   //因多个设备可以使用同一个驱动, 用链表来记录dev_t dev;         //设备号unsigned int count;    //设备数};

////////字符设备驱动//////////

1. 申请设备号

2. 定义一个cdev的设备驱动对象

struct cdev mycdev; //定义一个file_operations的文件操作对象struct file_operations fops = {.owner = THIS_MODULE,.read = 读函数....};

3. 把fops对象与mycdev关联起来

cdev_init(&mycdev, &fops); //mycdev.ops = &fops;mycdev.owner = THIS_MODULE;

4. 把设备驱动加入内核里, 并指定该驱动对应的设备号

cdev_add(&mycdev, 设备号, 次设备号的个数);

5. 卸载模块时, 要把设备驱动从内核里移除, 并把设备号反注册

cdev_del(&mycdev);///////////创建设备文件mknod /dev/设备文件名 c 主设备号 次设备号////////inode节点对象描述一个文件/设备文件, 包括权限,设备号等信息struct inode {...dev_t i_rdev;   //设备文件对应的设备号struct cdev *i_cdev; //指向对应的设备驱动对象的地址...};////file对象描述文件描述符, 在文件打开时创建, 关闭时销毁struct file {...const struct file_operations *f_op; //对应的文件操作对象的地址unsigned int f_flags; //文件打开的标志fmode_t f_mode; //权限loff_t f_pos;  //文件描述符的偏移struct fown_struct f_owner; //属于哪个进程unsigned int f_uid, f_gid; void *private_data; //给驱动程序员使用...};

通file里的成员f_path.dentry->d_inode->i_rdev可以获取到设备文件的设备号

///错误码在 ////

/////////struct file_operations ////

inode表示应用程序打开的文件的节点对象, file表示打开文件获取到的文件描述符

成功返回0, 失败返回错误码

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

buf指向用户进程里的缓冲区, len表示buf的大小(由用户调用read时传进来的)

off表示fl文件描述符的操作偏移, 返回值为实际给用户的数据字节数.

ssize_t (*read) (struct file *fl, char __user *buf, size_t len, loff_t *off);

用户进程把数据给驱动

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

to指用户进程的缓冲区, from指驱动里装数据的缓冲区, n多少字节, 返回值是0

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

to指驱动的... from用户... n多少字节, ....

static inline unsigned long __must_check copy_to_user(void __user *to, constvoid *from, unsigned long n){if (access_ok(VERIFY_WRITE, to, n))n = __copy_to_user(to, from, n);return n; //返回值为剩下多少字节没拷贝}
extern inline long copy_from_user(void *to, const void __user *from, long n)
  • 如果与用户进程交互的数据是1,2,4,8字节的话, 可用put_user(x,p) //x为值, p为地址

  • 如果从用户进程获取1,2,4字节的话, 可用get_user(x,p)

//////////////动态申请内存, 并清零. size为申请多大(不要超过128K),//flags为标志(常为GFP_KERNEL). 成功返回地址, 失败返回NULL// GFP_ATOMIC, 使用系统的内存紧急池void *kmalloc(size_t size, gfp_t flags);//申请后要内存要清零void *kzalloc(size_t size, gfp_t flags); //申请出来的内存已清零void kfree(const void *objp); //回收kmalloc/kzalloc的内存void *vmalloc(unsigned long size); //申请大内存空间void vfree(const void *addr); //回收vmalloc的内存// kmalloc申请出来的内存是物理地址连续的, vmalloc不一定是连续的///// container_of(ptr, type, member) type包括member成员的结构体,//ptr是type类型 结构体的member成员的地址.//此宏根据结构体成员的地址获取结构体变量的首地址#define container_of(ptr, type, member) ({ \const typeof( ((type *)0)->member ) *__mptr = (ptr); \(type *)( (char *)__mptr - offsetof(type,member) );})#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) 15 typedef struct led_dev_t { 16     dev_t mydevid; 17     unsigned int *rLEDCON; 18     unsigned int *rLEDDAT; 19     struct cdev mycdev; 20 }LED_DEV; LED_DEV myled; //ind->i_cdev是指向myled.mycdev成员的地址 //结构体变量myled首地址可由container_of(ind->i_cdev, LED_DEV, mycdev)获取;

/////// 自动创建设备文件 ////

#include 

1.

struct class *cl; cl = class_create(owner, name) ; //owner指属于哪个模块, name类名//创建出来后可以查看 /sys/class/类名void class_destroy(struct class *cls); //用于销毁创建出来的类

2. 创建设备文件

struct device *device_create(struct class *cls, struct device *parent,  dev_t devt, void *drvdata,  const char *fmt, ...)  __attribute__((format(printf, 5, 6)));device_create(所属的类, NULL, 设备号, NULL, "mydev%d", 88); //在/dev/目录下产生名字为mydev88的设备文件void device_destroy(struct class *cls, dev_t devt); //用于销毁创建出来的设备文件////////int register_chrdev(unsigned int major, const char *name,  const struct file_operations *fops) ; //注册设备号并创建驱动对象void unregister_chrdev(unsigned int major, const char *name); //反注册设备号并删除驱动对象static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops){return __register_chrdev(major, 0, 256, name, fops);}int __register_chrdev(unsigned int major, unsigned int baseminor,   unsigned int count, const char *name,   const struct file_operations *fops){struct char_device_struct *cd;struct cdev *cdev;int err = -ENOMEM;cd = __register_chrdev_region(major, baseminor, count, name);if (IS_ERR(cd))return PTR_ERR(cd);cdev = cdev_alloc();if (!cdev)goto out2;cdev->owner = fops->owner;cdev->ops = fops;kobject_set_name(&cdev->kobj, "%s", name);err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);if (err)goto out;cd->cdev = cdev;return major ? 0 : cd->major;out:kobject_put(&cdev->kobj);out2:kfree(__unregister_chrdev_region(cd->major, baseminor, count));return err;}

看完了这篇文章,相信你对"Linux内核设备驱动之字符设备驱动的示例分析"有了一定的了解,如果想了解更多相关知识,欢迎关注行业资讯频道,感谢各位的阅读!

设备 文件 驱动 内核 结构 用户 空间 数据 字符 函数 设备驱动 分配 主设备 地址 字节 对象 指针 内存 进程 拷贝 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 伟宏互联网科技 工业互联网科技新基建 金诺互联网科技 科技与互联网发展 平安科技 互联网 奇酷互联网络科技公司 善班互联网科技有限公司 互联网科技公司招聘 富国互联网科技创新基金 科技与互联网的利与弊 青岛中康互联网医疗科技有限公司 厦门千工互联网科技有限公司 知域互联网科技有限公司大骗子 电子科技大学互联网创业大赛 乌鲁木齐新华智原互联网科技学校 群盈汇互联网科技有限公司 山东星卡互联网科技有限公司官网 抖集互联网科技技术咨询 利用互联网等科技手段 互联网科技快速发展优美句子 深圳九邦互联网科技有限公司 云信互联网科技有限公司 互联网科技官网 互联网金融监管科技数据 科技信息互联网 麻工互联网科技 深圳卡尔迅互联网科技有限公司怎么样 广州恒生互联网科技有限 九思连强互联网电子科技有限公司 长沙渺峰互联网科技有限公司
0