分享

Linux字符设备驱动框架(设备号是一个32位的整数用来唯一标识Linux系统中的所有设备)

 山峰云绕 2023-05-07 发布于贵州

   (设备号是一个32位的整数用来唯一标识Linux系统中的所有设备)


https://m.toutiao.com/is/D3gS1Uc/?= 


字符设备驱动是Linux内核中的一种设备驱动类型,主要用于管理字符设备的输入输出操作。如终端设备、串口、打印机、键盘等都是字符设备。与块设备驱动不同的是,字符设备驱动不需要缓存数据,因此非常适合那些不需要频繁的数据访问的设备。

一般而言,一个完整的字符设备驱动包含以下几个部分:设备初始化、设备注册、文件操作函数定义、中断处理函数定义、设备卸载等。

一、设备初始化

设备初始化是字符设备驱动的第一步,其主要任务是在内核中分配设备号并将字符设备驱动注册到内核中。设备号是一个32位的整数,用来唯一标识Linux系统中的所有设备,不同的设备应使用不同的设备号。系统中的前256个设备号被预留给Linux内核使用,因此对于第三方的字符设备来说,应该选择一个未被预留的设备号。

为了分配设备号,我们需要定义一个struct cdev对象,并在其中填充设备号、文件操作函数表等相关信息。例如:

struct cdev mydev;

static struct file_operations mydev_fops = {

.owner = THIS_MODULE,

.read = mydev_read,

.write = mydev_write,

.open = mydev_open,

.release = mydev_release,

};

static int __init mydev_init(void)

{

int ret;

dev_t devno;

// 分配设备号

if (alloc_chrdev_region(&devno, 0, 1, 'mydev') < 0) {

printk(KERN_ERR 'alloc_chrdev_region failed\n');

return -1;

}

// 初始化cdev对象

cdev_init(&mydev, &mydev_fops);

mydev.owner = THIS_MODULE;

// 注册字符设备驱动到内核

ret = cdev_add(&mydev, devno, 1);

if (ret < 0) {

printk(KERN_ERR 'cdev_add failed\n');

unregister_chrdev_region(devno, 1);

return -1;

}

printk(KERN_INFO 'mydev driver initialized\n');

return 0;

}

上面的代码中,我们首先定义了一个struct cdev对象mydev,并将其与文件操作函数表mydev_fops相关联。然后通过alloc_chrdev_region函数分配一个设备号,并将其保存在devno中。接着我们调用cdev_init函数对mydev进行初始化,并将其owner成员指向当前模块。

最后,我们使用cdev_add函数将mydev注册到内核中,并检查其返回值,如果返回值小于0则表示注册失败。当设备注册成功后,我们可以使用cat /proc/devices命令来查看系统中所有的字符设备,其中包括我们刚刚注册的设备。

二、设备注册

设备注册是指将字符设备驱动注册到系统中的设备接口列表中。在注册设备之前,我们需要定义一个struct class对象,并在其中填充设备类名等相关信息,例如:

struct class *mydev_class;

static int __init mydev_init(void)

{

...

// 创建设备类

mydev_class = class_create(THIS_MODULE, 'mydev_class');

if (IS_ERR(mydev_class)) {

printk(KERN_ERR 'class_create failed\n');

cdev_del(&mydev);

unregister_chrdev_region(devno, 1);

return -1;

}

// 创建设备文件

device_create(mydev_class, NULL, devno, NULL, 'mydev%d', 0);

printk(KERN_INFO 'mydev driver initialized\n');

return 0;

}

上面的代码中,在成功分配设备号并将mydev注册到内核中之后,我们通过class_create函数创建了一个设备类mydev_class。设备类用于将同一类型的设备分组,例如所有的串口设备可以放在一个设备类中。

然后我们使用device_create函数创建了设备文件,将设备号devno与设备类mydev_class进行关联,为设备文件命名为mydev%d,其中%d表示设备文件的编号,这里我们只创建了一个设备文件,因此参数为0。

在设备文件创建成功后,我们可以在/dev目录下看到mydev0文件。

三、文件操作函数

文件操作函数是字符设备驱动中最为重要的部分,它定义了对设备的读写操作、设备打开和关闭以及设备的控制操作。在字符设备驱动中,每个文件操作函数都对应着一个文件操作符,因此任何对该设备的操作都会触发相应的文件操作函数。

字符设备驱动中文件操作函数的定义如下:

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 *);

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

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

int (*flush) (struct file *, fl_owner_t id);

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

};

其中,llseek函数用于控制读写指针的位置,read函数用于从设备中读数据,write函数用于向设备中写数据,open和release函数用于打开和关闭设备。

这里我们给出一个简单的示例,实现从设备中读取10个字节的数据。

static ssize_t mydev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)

{

char kbuf[10] = 'abcdefghi';

int ret = 0;

if (*f_pos > 9)

return 0;

if (count + *f_pos > 10)

count = 10 - *f_pos;

if (copy_to_user(buf, kbuf + *f_pos, count))

return -EFAULT;

*f_pos += count;

ret = count;

return ret;

}

上面的代码中,我们定义了一个kbuf数组来存储数据。然后我们根据当前读取的位置f_pos,计算出需要读取的字节数count,并使用copy_to_user函数将数据复制到用户空间。

最后我们更新读取的位置f_pos,并返回读取的字节数。

四、中断处理函数

在字符设备驱动中,有些硬件设备会产生中断,这时候需要定义相应的中断处理函数来处理中断事件。Linux中的中断机制非常强大,可以处理各种类型的中断事件,包括定时器中断、硬件中断、软中断等。

在字符设备驱动中,我们需要定义一个中断处理函数,并将其注册到相应的中断号上。例如:

static irqreturn_t mydev_interrupt(int irq, void *dev_id)

{

printk(KERN_INFO 'mydev_interrupt\n');

return IRQ_HANDLED;

}

static int __init mydev_init(void)

{

...

// 注册中断

if (request_irq(IRQ_NUMBER, mydev_interrupt, IRQF_SHARED, 'mydev', &mydev)) {

printk(KERN_ERR 'request_irq failed\n');

device_destroy(mydev_class, devno);

class_destroy(mydev_class);

cdev_del(&mydev);

unregister_chrdev_region(devno, 1);

return -1;

}

printk(KERN_INFO 'mydev driver initialized\n');

return 0;

}

上面的代码中,我们定义了一个mydev_interrupt函数来处理中断事件。在设备初始化中,我们通过request_irq函数将该中断处理函数注册到中断号IRQ_NUMBER上,并将mydev作为传递给中断处理函数的参数。其中,IRQF_SHARED表示该中断可以被多个驱动程序共享。

在进入中断处理函数后,我们可以对设备进行相应的操作,例如读取已经接收到的数据、清除中断标志等。最后,我们需要使用IRQ_HANDLED返回一个中断事件已经被成功处理的标志。

五、设备卸载

设备卸载需要释放所有已经分配的资源,包括设备号、设备文件、设备类、中断等。一个完整的设备卸载函数应该包含以下几个步骤:

static void __exit mydev_exit(void)

{

// 释放中断

free_irq(IRQ_NUMBER, &mydev);

// 销毁设备文件和设备类

device_destroy(mydev_class, devno);

class_destroy(mydev_class);

// 从内核中注销字符设备驱动

cdev_del(&mydev);

unregister_chrdev_region(devno, 1);

printk(KERN_INFO 'mydev driver exited\n');

}

module_exit(mydev_exit);

上面的代码中,我们依次使用free_irq、device_destroy、class_destroy、cdev_del和unregister_chrdev_region函数来释放各种资源。最后我们使用module_exit宏定义来注册设备卸载函数mydev_exit。当该模块被卸载时,mydev_exit函数将被自动调用。

总结

本文详细介绍了字符设备驱动框架的实现,包括设备初始化、设备注册、文件操作函数定义、中断处理函数定义和设备卸载等。字符设备驱动是Linux内核中非常重要的一部分,通过学习字符设备驱动的实现,可以帮助我们更好地理解Linux内核中设备驱动的工作原理。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多