- 学习linux 字符驱动
1.Introduction
Hardware devices are accessed by the user through special device files. These files are grouped into the /dev directory, and system calls open, read, write, close, lseek, mmap etc. are redirected by the operating system to the device driver associated with the physical device. The device driver is a kernel component (usually a module) that interacts with a hardware device.
There are two categories of device files and thus device drivers: character and block. This division is done by the speed, volume and way of organizing the data to be transferred from the device to the system and vice versa.
They are differences:
-
character device
There are slow devices, which manage a small amount of data, and access to data does not require frequent seek queries. Examples are devices such as keyboard, mouse, serial ports, sound card, joystick. In general, operations with these devices (read, write) are performed sequentially byte by byte. -
block device
Data volume is large, data is organized on blocks, and search is common. Examples of devices are hard drives, cdroms, ram disks, magnetic tape drives. For these devices, reading and writing is done at the data block level.
If for character devices system calls go directly to device drivers, in case of block devices, the drivers do not work directly with system calls. In the case of block devices, communication between the user-space and the block device driver is mediated by the file management subsystem and the block device subsystem. The role of these subsystems is to prepare the device driver’s necessary resources (buffers), to keep the recently read data in the cache buffer, and to order the read and write operations for performance reasons.
2.字符设备框架
2.1.设备号
Linux系统中,使用dev_t类型来标识一个设备号,它是一个32位的无符号整数,其中,12位为主设备号,20位为次设备。
<include linux/types.h>
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
随着内核版本的演变,上述的主次设备号的构成也许会发生变化,所以设备驱动开发者应该避免直接使用主次设备号所占的位宽来获得对应的主设备号或者次设备号。内核为了保证在主次设备号位宽发生变化时,现在的程序依然可以工作,内核提供了如下的几个宏:
<include /linux/kdev_t.h>
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
- MAJOR是用来从一个dev_t 类型的设备号中提取出主设备号;
- MINOR用来提取次设备号;
- MKDEV则是将主设备号ma和次设备号mi合成一个dev_t类型的设备号。
主设备号定位对应的设备驱动程序(即是主设备找驱动),次设备号标识它同个驱动所管理的若干的设备(次设备号找设备)。因此,从这个角度来看,设备号作为系统资源,必须要进行仔细的管理,以防止设备号与驱动程序的错误对应所带来的管理设备的混乱。
The devices traditionally had a unique, fixed identifier associated with them. This tradition is preserved in Linux, although identifiers can be dynamically allocated (for compatibility reasons, most drivers still use static identifiers). The identifier consists of two parts: major and minor.
- Majors:identifies the device type (IDE disk, SCSI disk, serial port, etc.) -
- Minors:identifies the device (first disk, second serial port, etc.). Most times, the major identifies the driver, while the minor identifies each physical device served by the driver. In general, a driver will have a major associate and will be responsible for all minors associated with that major.
2.2.设备号分配
Certain major identifiers are statically assigned to devices (in the Documentation/admin-guide/devices.txt file from the kernel sources). When choosing the identifier for a new device, you can use two methods:
- static (choose a number that does not seem to be used already)
- dynamically
2.2.1.静态分配
- register_chrdev_region
用于静态注册设备号,优点是可以在注册的时候就知道其设备号,缺点是可能会与系统中已经注册的设备号冲突导致注册失败。
函数分析如下:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
struct char_device_struct *cd;
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
内核中所有已分配的字符设备编号都记录在一个名为 chrdevs 散列表里。该散列表中的每一个元素是一个 char_device_struct 结构,它的定义如下:
<fs/char_dev.c>
static struct char_device_struct {
struct char_device_struct *next; // 指向散列冲突链表中的下一个元素的指针
unsigned int major; // 主设备号
unsigned int baseminor; // 起始次设备号
int minorct; // 设备编号的范围大小
char name[64]; // 处理该设备编号范围内的设备驱动的名称
struct file_operations *fops; // 没有使用
struct cdev *cdev; // 指向字符设备驱动程序描述符的指针
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
#define CHRDEV_MAJOR_HASH_SIZE 255
该数组的每一项都是一个指向struct char_device_struct类型的指针。系统刚开始运行时,数组的初始化状态:
register_chrdev_region函数完成的主要功能:
将当前设备驱动程序要使用的设备记录到chrdevs数组中,而有了这种对设备号使用情况的跟踪,系统就可以避免不同的设备驱动程序使用同一个设备号的情况出现。这就意味着当驱动程序调用这个函数时,事先已经明确知道他要使用的设备号,之所以调用这个函数,是要将所管理的设备号纳入到内核的设备号管理体系中,防止被的驱动程序错误使用到。当然如果它试图使用的设备号已经被之前某个驱动程序使用了,调用将会失败,register_chrdev_region将返回错误码给调用者,如果调用成功。
register_chrdev_region中的核心函数是__register_chrdev_region,该函数首先分配一个struct char_device_struct类型的对象cd,然后对其进行一些初始化工作。这个过程完成之后,它就开始搜索chrdevs数组,是通过哈希表的形式进行的,首先会通过主设备号生成一个散列关键值:
i = major_to_index(major);
其中CHRDEV_MAJOR_HASH_SIZE = 255。
static inline int major_to_index(unsigned major)
{
return major % CHRDEV_MAJOR_HASH_SIZE;
}
此后,函数将对chrdevs[i]元素管理的链表进行扫描,如果chrdevs[i]上已经有了链表节点,表明之前有别的驱动程序使用的主设备号散列到chrdevs[i]上,为此函数就需要响应的逻辑确保当前正在操作的设备号不会与这些已经使用的设备号发生冲突,如果有冲突函数返回错误码,表明本次调用失败。如果本次调用使用的设备号与chrdevs[i]上已经有的设备号没有发生冲突,先前分配的struct char_device_struct对象cd将加入到chrdevs[i]领衔的链表中成为一个新的节点。
2.2.2.动态分配
- alloc_chrdev_region
系统协助动态分配设备号,分配的主设备号的范围在1-254之间。
3.字符设备数据结构
In the kernel, a character-type device is represented by struct cdev, a structure used to register it in the system. Most driver operations use three important structures:
- struct file_operations
- struct file
- struct inode
3.1.struct cdev
include/linux/cdev.h:
14 struct cdev {
15 struct kobject kobj;
16 struct module *owner;
17 const struct file_operations *ops;
18 struct list_head list;
19 dev_t dev;
20 unsigned int count;
21 } __randomize_layout;
3.2.struct file_operations
As mentioned above, the character device drivers receive unaltered system calls made by users over device-type files. Consequently, implementation of a character device driver means implementing the system calls specific to files: open, close, read, write, lseek, mmap, etc. These operations are described in the fields of the struct file_operations structure:
#include <linux/fs.h>:
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 *);
[...]
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
[...]
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
[...]
It can be noticed that the signature of the function differs from the system call that the user uses. The operating system sits between the user and the device driver to simplify implementation in the device driver.
open does not receive the parameter path or the various parameters that control the file opening mode. Similarly, read, write, release, ioctl, lseek do not receive as a parameter a file descriptor. Instead, these routines receive as parameters two structures: file and inode. Both structures represent a file, but from different perspectives.
Most parameters for the presented operations have a direct meaning:
- file and inode identifies the device type file;
- size is the number of bytes to be read or written;
- offset is the displacement to be read or written (to be updated accordingly);
- user_buffer user buffer from which it reads / writes;
- whence is the way to seek (the position where the search operation starts);
- cmd and arg are the parameters sent by the users to the ioctl call (IO control).
3.3.inode and file structures
An inode represents a file from the point of view of the file system. Attributes of an inode are the size, rights, times associated with the file. An inode uniquely identifies a file in a file system.
The file structure is still a file, but closer to the user’s point of view. From the attributes of the file structure we list: the inode, the file name, the file opening attributes, the file position. All open files at a given time have associated a file structure.
To understand the differences between inode and file, we will use an analogy from object-oriented programming: if we consider a class inode, then the files are objects, that is, instances of the inode class. Inode represents the static image of the file (the inode has no state), while the file represents the dynamic image of the file (the file has state).
Returning to device drivers, the two entities have almost always standard ways of using: the inode is used to determine the major and minor of the device on which the operation is performed, and the file is used to determine the flags with which the file was opened, but also to save and access (later) private data.
The file structure contains, among many fields:
- f_mode, which specifies read FMODE_READ (FMODE_READ) or write (FMODE_WRITE);
- f_flags, which specifies the file opening flags (O_RDONLY, O_NONBLOCK, O_SYNC, O_APPEND, O_TRUNC, etc.);
- f_op, which specifies the operations associated with the file (pointer to the file_operations structure );
- private_data, a pointer that can be used by the programmer to store device-specific data; The pointer will be initialized to a memory location assigned by the programmer.
- f_pos, the offset within the file
The inode structure contains, among many information, an i_cdev field, which is a pointer to the structure that defines the character device (when the inode corresponds to a character device).
4.字符设备注册
相关函数如下所示:
- cdev_init – initialize a cdev structure
- cdev_alloc – allocate a cdev structure
- cdev_add – add a char device to the system
- cdev_del – remove a cdev from the system
使用cdev_add注册字符设备前应该先调用register_chrdev_region或alloc_chrdev_region分配设备号。
接下来进行cdev结构体初始化,一般有两种定义方式:静态的和动态的。
- 静态内存定义初始化:
struct cdev my_cdev;
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
- 动态内存定义初始化:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;
注意:cdev_alloc函数针对于需要空间申请的操作,而cdev_init针对于不需要空间申请的操作;因此如果你定义的是一个指针,那么只需要使用cdev_alloc函数并在其后做一个ops的赋值操作就可以了;如果你定义的是一个结构体而非指针,那么只需要使用cdev_init函数就可以了。两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的数据结构需求而定。
最后调用 cdev_add() 函数,把它添加到系统中去。
478 int cdev_add(struct cdev *p, dev_t dev, unsigned count)
479 {
480 int error;
481
482 p->dev = dev;
483 p->count = count;
484
485 error = kobj_map(cdev_map, dev, count, NULL,
486 exact_match, exact_lock, p);
487 if (error)
488 return error;
489
490 kobject_get(p->kobj.parent);
491
492 return 0;
493 }
该函数关键核心函数是 kobj_map,该函数负责把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里。字符设备驱动定义一个static struct kobj_map *cdev_map; 内核中所有字符设备都会记录在一个该cdev_map 变量中,该变量包含一个散列表用来快速存取所有的对象。如果要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。
drivers/base/map.c:
struct kobj_map {
struct probe {
struct probe *next;
dev_t dev;
unsigned long range;
struct module *owner;
kobj_probe_t *get;
int (*lock)(dev_t, void *);
void *data;
} *probes[255];
struct mutex *lock;
};
相关函数操作集:
typedef struct kobject *kobj_probe_t(dev_t, int *, void *);
struct kobj_map;
int kobj_map(struct kobj_map *, dev_t, unsigned long, struct module *,
kobj_probe_t *, int (*)(dev_t, void *), void *);
void kobj_unmap(struct kobj_map *, dev_t, unsigned long);
struct kobject *kobj_lookup(struct kobj_map *, dev_t, int *);
struct kobj_map *kobj_map_init(kobj_probe_t *, struct mutex *);
重点介绍:
kobj_map函数中哈希表的实现原理和注册分配设备号中的几乎完全一样,通过要加入系统的设备的主设备号major(major=MAJOR(dev))来获得probes数组的索引值i(i = major % 255),然后把一个类型为struct probe的节点对象加入到probes[i]所管理的链表中,如下图所示。其中struct probe记录了当前正在加入系统的字符设备对象的有关信息。其中,dev是它的设备号,range是从次设备号开始连续的设备数量,data是一void *变量,指向当前正要加入系统的设备对象指针p。下图展示了两个满足主设备号major % 255 = 2的字符设备通过调用cdev_add之后,cdev_map所展现出来的数据结构状态。
所以,设备驱动程序通过调用cdev_add把它所管理的设备对象的指针嵌入到一个类型为struct probe的节点之中,然后再把该节点加入到cdev_map所实现的哈希链表中。
对系统而言,当设备驱动程序成功调用了cdev_add之后,就意味着一个字符设备对象已经加入到了系统,在需要的时候,系统就可以找到它。对用户态的程序而言,cdev_add调用之后,就已经可以通过文件系统的接口调用到驱动程序。
当一个字符设备驱动不再需要的时候(比如模块卸载),就可以用 cdev_del() 函数来释放 cdev 占用的内存。
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}
cdev_unmap() 调用 kobj_unmap() 来释放 cdev_map 散列表中的对象。kobject_put() 释放 cdev 结构本身。
5.建立设备文件
Linux中设备节点是通过“mknod”命令来创建的。一个设备节点其实就是一个文件,Linux中称为设备文件。有一点必要说明的是,在Linux中,所有的设备访问都是通过文件的方式,一般的数据文件程序普通文件,设备节点称为设备文件。该文件存在的意义是沟通用户空间程序和内核空间驱动程序。linux系统所有的设备文件都位于/dev目录下。
To create a device type file, use the mknod command; the command receives the type (block or character), major and minor of the device (mknod name type major minor). Thus, if you want to create a character device named mycdev with the major 42 and minor 0, use the command:
$ mknod /dev/mycdev c 42 0
使用strace跟踪该命令:
$ strace mknod /dev/mycdev c 42 0
execve("/bin/mknod", ["mknod", "/dev/mycdev", "c", "42", "0"], [/* 33 vars */]) = 0
...
mknod("/dev/mycdev", S_IFCHR|0666, makedev(42, 0)) = -1 EEXIST (File exists)
执行mknod命令,最终通过调用mknod函数实现,调用时重要参数有:
- 设备文件名(/dev/mycdev):主要在用户空间使用
- 设备号(makedev(42, 0))。
To create the block device with the name mybdev with the major 240 and minor 0 the command will be:
$ # mknod /dev/mybdev b 240 0
执行insmod *.ko,查看系统注册的字符设备:
~/Documents/work/code/linux/linux-4.19.37$ cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
5 ttyprintk
6 lp
7 vcs
10 misc
13 input
14 sound/midi
14 sound/dmmidi
...
6.Example
char_dev.c:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
static struct cdev chr_dev;
static dev_t ndev;
static int chr_open(struct inode *nd, struct file *filp)
{
int major = MAJOR(nd->i_rdev);
int minor = MINOR(nd->i_rdev);
printk("chr_open, major = %d, minor = %d\n",major, minor);
return 0;
}
static ssize_t chr_read(struct file *f, char __user *u, size_t sz, loff_t *off)
{
printk("in the chr_read() function!\n");
return 0;
}
struct file_operations chr_ops = {
.owner = THIS_MODULE,
.open = chr_open,
.read = chr_read,
};
static int demo_init(void)
{
int ret;
cdev_init(&chr_dev, &chr_ops);
ret = alloc_chrdev_region(&ndev, 0, 1, "chr_dev");
if (ret < 0)
return ret;
printk("albert:major = %d,minor = %d\n", MAJOR(ndev), MINOR(ndev));
ret = cdev_add(&chr_dev, ndev, 1);
if (ret < 0)
return ret;
return 0;
}
static void demo_exit(void)
{
printk("removing chr_dev module!\n");
cdev_del(&chr_dev);
unregister_chrdev_region(ndev, 1);
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
Makefile:
1 obj-m += char_dev.o
2
3 KDIR = /lib/modules/$(shell uname -r)/build
4 #KDIR = /lib/modules/4.4.0-141-generic/build
5
6 all:
7 make -C $(KDIR) M=$(shell pwd) modules
8
9 clean:
10 make -C $(KDIR) M=$(shell pwd) clean
测试应用程序:
char_dev_test.c:
#include <stdio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int my_dev = open("/dev/demo_device", 0);
if (my_dev < 0) {
perror("fail to open device file.");
} else {
printf("albert:%s\n",__func__);
ioctl(my_dev, 100, 110);
close(my_dev);
}
return 0;
}
编译:
gcc char_dev_test.c -o char_dev_test
测试步骤:
1.sudo insmod char_dev.ko
2.sudo ./char_dev_test
3.dmeg
参考:
https://blog.csdn.net/liebao_han/article/details/78931683
https://linux-kernel-labs.github.io/master/labs/device_drivers.html