岁虚山

行有不得,反求诸己

0%

字符设备驱动程序与访问进阶

上一章中,实现了一个简单的字符设备驱动程序,并对其进行了 openclose 的操作。对于一个字符设备驱动程序而言,打开和关闭仅仅是最基础的操作,设备驱动程序还能够通过其他的操作,来实现更多的功能。

本章内容,将实现一个具备更多功能的设备驱动程序,来了解字符设备驱动程序的功能。

概述

对字符设备驱动程序的操作,其实是对字符设备节点文件进行操作。

在打开一个字符设备后,系统会为打开字符设备的用户程序创建并维护一个 struct file 结构体,该结构体指向了实际存在的 inode 节点。用户程序对字符设备进行的所有操作,都是通过系统内的 struct file 结构体来进行的。其中 struct file 结构体中包含有一个 struct file_operations* 类型的字段 f_opstruct file_operations 的结构体中包含了对字符设备进行各类操作的定义。

用户程序对字符设备所进行的操作,都对应地通过 f_op 变量来找到字符设备驱动程序中所实现的函数并执行。

user_and_kernel_file_operations

设备操作

file_opeartions

因此,为了使得字符设备实现更多的功能,就要根据 struct file_operations 结构体各字段的函数指针,来实现对应的函数。

对此,我们要熟悉 struct file_operations 所包含各个函数指针字段所指向的函数的含义。

先来看一下 struct file_operations 结构体的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// in 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 *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*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 *);
unsigned long mmap_supported_flags;
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 (*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 (*setfl)(struct file *, unsigned long);
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 **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

以上是在 5.15 版本内核中对 struct file_operations 结构体的完整定义。本篇不会对结构体内的所有函数进行解释,而是挑选一些典型的常用函数进行说明。

llseek 函数

1
loff_t (*llseek) (struct file *, loff_t, int);

llseek 函数用来修改文件的当前读写位置,并将新的位置作为返回值进行返回。当函数执行出错时,返回一个负数。用户程序中,对 lseek 函数的调用最终执行的就是该函数。

该函数接受三个参数,完整的定义如下:

1
loff_t (*llseek) (struct file *pfile, loff_t offset, int whence);

pfile 参数为一个 struct file * 类型的指针,指向了被读取的文件的 struct file 结构体;

loff_t 参数为一个长偏移量,指的是相对于当前文件的读写位置,所要便偏移的地址,可以为正,代表将读写位置置后;也可以为负,代表将读写位置提前。一般来讲,即使在 32 位的机器上面,loff_t 类型也至少占用 64 位的数据宽度。

对于所谓的 “读写位置”,其实在 struct file 结构体中存在一个 loff_t 类型的 f_pos 字段,记录该文件的当前的读写位置。使用传统意义上的文件作一个类比,假如打开一个 2k 大小的文本文件,此时,代表该文件的 file 结构体中的 f_pos 为 0,第一次读取 20 个字节,那么真实读取出的数据是文件的前 20 个字节的数据, f_pos 此时的值就为 20。第二次读取 30 个字节的数据,根据 f_pos 的值,从 f_pos 的后开始读取 30 个字节,那么真正读取出来的数据就是文件的第 21 个字节到第 50 个字节,共 30 个数据,此时 f_pos 的值为 50。

要注意的是,一个 file 中只有一个 f_pos 字段,该字段是对于所有的操作都生效的。比如说,打开一个文件后,先读取 20 个字节,此时 f_pos = 20,如果再写入 15 个字节的数据,那么会从文件的第 21 个字节开始写入 15 个字节,此时 f_pos = 35

因此,该函数主要用来对文件的操作位置进行设置,如已经读取了该文件的部分数据,想要从头开始重新读取文件,就可以通过调用 lseek 函数来将读写位置 f_pos 置为 0。

并不是所有的操作都会改变 f_pos 变量的值,这将在后续的涉及到修改 f_pos 值的函数中进行说明。

whence 代表移动光标的参考位置,有3种:

SEEK_SET : 从文件的开头位置计算偏移量

SEEK_CUR : 从当前的位置开始计算偏移量

SEEK_END : 从文件的结尾开始计算偏移量

该函数返回非负数时表示当前文件的指针位置,返回负数表示函数调用失败。

当该函数指针被置为 NULL 时,用户程序调用 seek 函数会以一种不可预期的方式修改 file 结构。

read 函数

1
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

read 函数用于从设备中读取数据,当该函数指针被设置为 NULL 时,将导致系统调用失败出错并返回并返回 - EINVAL(Invalid argument,非法参数) 。当函数调用成功时,返回成功读取的字节数,否则返回一个负值。

该函数接收四个参数,函数的完整定义为:

1
ssize_t (*read) (struct file *pfile, char __user *buff, size_t length, loff_t *position);

pfile 参数为一个 struct file * 类型的指针,指向了被读取的文件的 struct file 结构体;

buff 参数为用户空间中的字符串指针,从设备中读取出的数据就保存在该字符串中。注意,该参数使用了 __user 关键字进行声明,表明这个指针指向的是一片用户空间中的地址。为了能够更清晰地对此进行说明,本章文末大致介绍了用户空间和内核空间的概念,在此就不作过多的展开,而是直接使用一个重要的概念:内核空间的程序无法随意访问用户空间。

字符设备驱动程序运行在内核空间中,用户程序调用 read 函数时,buff 参数指向的是一段用户空间中的内存,字符设备驱动程序为了能够将数据写入 buff 所指向的内存中,需要借助系统提供的 copy_to_user 函数。该函数原型如下:

1
int copy_to_user(void __user *to, const void *from, unsigned long n)

其中, to 参数指向目标地址,一般来讲就是用户空间的地址,即 buff 参数所指向的地址;from 参数指向源地址,一般指设备内部的内存;n 指要复制的数据的长度,单位是字节。

该函数的作用就是从 from(内核空间) 复制 n 个字节的数据到 to(用户空间) 中。

若该函数执行成功,返回 0,否则返回未能拷贝成功的字节数。

length 参数为读取数据的大小,单位为长度。

loff_t 参数为被操作的文件当前的 f_pos 指针的值。

看到这里,可能会对 read 函数产生疑问。因为一般在编写用户程序时,读取文件所使用的 read 函数原型是这样的:

1
ssize_t read (int __fd, void *__buf, size_t __nbytes);

__fd 为打开文件时返回的文件号;

__buf 为读取出的数据要存放的地址;

__nbytes 为要读取的字节数。

用户程序中的 read 函数与设备的 read 函数的参数似乎不太一致?

这是因为设备的 struct file_operations 结构体中的 read 函数是对 “读取” 这个操作更底层,更具体的实现。用户程序中的 read 其实最终是调用了设备的 read 函数,二者只是名字一样,实际上是完全不同的两个函数。那么在用户程序中调用 read 函数时,到底发生了什么呢?

为了了解用户程序调用 read 函数执行的过程,最好的方式是查看 Linux 的源码。

当用户程序调用 read 函数时,系统会通过 sys_read 函数来进行处理,在当前内核版本中 sys_read 函数定义在 /fs/read_write.c 文件中被定义为:中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;

if (f.file) {
loff_t pos, *ppos = file_ppos(f.file);
if (ppos) {
pos = *ppos;
ppos = &pos;
}
ret = vfs_read(f.file, buf, count, ppos);
if (ret >= 0 && ppos)
f.file->f_pos = pos;
fdput_pos(f);
}
return ret;
}

在第 7 行代码中,ksys_read 函数使用传入的 fd 参数获取到了 struct fd 结构体,该结构体定义在 /linux/file.h 中:

1
2
3
4
struct fd {
struct file *file;
unsigned int flags;
};

该结构体包含一个 struct file 类型的指针,该指针就指向被操作的文件在系统的中 struct file 数据结构。

在代码的第 7-11 行,通过 struct file 指针获取到了文件当前的偏移量 f_pos,然后在第 12 行,调用了 vfs_read 函数。

vfs_read 函数同样实现在 /fs/read_write.c 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;

if (!(file->f_mode & FMODE_READ))
return -EBADF;
if (!(file->f_mode & FMODE_CAN_READ))
return -EINVAL;
if (unlikely(!access_ok(buf, count)))
return -EFAULT;

ret = rw_verify_area(READ, file, pos, count);
if (ret)
return ret;
if (count > MAX_RW_COUNT)
count = MAX_RW_COUNT;

if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);
else if (file->f_op->read_iter)
ret = new_sync_read(file, buf, count, pos);
else
ret = -EINVAL;
if (ret > 0) {
fsnotify_access(file);
add_rchar(current, ret);
}
inc_syscr(current);
return ret;
}

可以看到,在 vfs_read 函数的第 18-19 行,当 struct file_operationsread 指针不为 NULL 时,最终调用了我们所编写的 read 函数。

同时,在 ksys_read 函数中,当 vfs_read 函数执行结束之后,根据 vfs_read 传出的 ppos 的值,修改了 struct file 数据结构的 f_pos 的值。因此,我们在编写字符设备驱动的 read 函数时,要注意根据 position 参数的值来确定开始读取的位置,并在读取成功后修改 position 的值,以供系统修改 struct file 数据结构的 f_pos** 的值。

read_iter 函数

1
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);

read_iter 异步读函数,即当用户程序调用该函数时,用户程序不会等待该函数执行结束,而是调用后立即执行后续的操作。当该函数置指针为 NULL 时,用户程序对该函数的调用将通过 read(同步) 函数来处理。

在较老的 Linux 版本中,异步读取的函数声明为:

1
ssize_t (*aio_read)(struct kiocb *, char __user*, size_t, loff_t);

write 函数

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

write 函数用于向设备写入数据,与 read 函数类似,当该函数指针为 NULL 时,调用失败出错并返回并返回 - EINVAL。当函数调用成功时,返回成功读写入的字节数,否则返回一个负值。

read 类似,写操作相当于从用户空间复制数据到内核空间,要使用 copy_from_user 函数来将用户空间的数据写入内核空间中,并在完成写操作后,更新 position 的值。

write_iter 函数

1
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);

write_iter 异步写函数,即当用户程序调用该函数时,用户程序不会等待该函数执行结束,而是调用后立即执行后续的操作。当该函数指针置为 NULL 时,用户程序对该函数的调用将通过 write(同步) 函数来处理。

在较老的 Linux 版本中,异步写入的函数声明为:

1
ssize_t (*aio_write)(struct kiocb *, char __user*, size_t, loff_t);

mmap 函数

1
int (*mmap) (struct file *, struct vm_area_struct *);

mmap 函数用于将设备内存映射到用户程序的进程空间中,这样的话用户程序可以直接对映射的内存进行的操作就是直接对设备的内存空间进行操作,从而免去对文件进行读写的操作。这样的好处可以免去用户空间和内核空间的拷贝过程,在对大量数据进行操作时,可以节省时间。

mmap

若该函数指针为 NULL ,那么 mmap 系统调用将返回 -ENODEV 错误。

open 函数

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

open 函数打开一个设备,该函数是用户程序对设备所进行其他操作的前提,只有当一个设备被打开后,才能够对其进行读写等操作。若该函数指针被设为 NULL,则当用户程序打开设备时,永远能够成功打开,但系统不会通知驱动程序,也就是说驱动程序无法对用户程序的打开操作做出任何反应。

flush 函数

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

flush 函数的作用是:当进程关闭设备文件描述符副本时,执行并等待设备上尚未完结的操作。当该函数被置为 NULL 时,内核将简单的忽略用户程序的请求。

release 函数

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

file 结构被释放时, 将执行 release 函数。该函数指针被置为 NULL 时,关闭设备时总是能够成功,但是无法通知到设备。

一般来讲,release 函数与 flush 都是用来当 file 结构被关闭时,进行 “扫尾” 的工作。但是两者不同的是, flush 函数发生在每一个 file 结构的副本被关闭时调用,而 release 函数会等到所有的副本都被关闭之后才回得到调用。

举例来讲,假如用户程序打开了一个设备,那么系统中维护了一个该设备的设备文件描述符 file 结构,暂时称其为 file_0。用户程序通过 forkdup 的方式,复制出了多个该设备描述符的副本 file_1file_2file_3,此时系统中虽然有四个 file 结构体,但是其实指向的都是同一个打开的设备文件,这四个 file 结构互为副本(即使 file_1file_2file_3 是由 file_0 复制而来,但是这四个 file 结构地位是完全等价的)。

所谓 “同一个打开的设备文件” 有两层含义。第一层含义指的是同一个设备文件;第二层含义是指这些 file 结构是因为同一次 “打开” 的操作而产生的。这与同一个设备文件通过多次 open 操作而产生的多个 file 结构是不同的。

针对这些副本,在每一个副本被关闭时,都会调用 flush 函数;而只有当所有的副本全部被关闭时,才会调用 release 函数。

fsync 函数

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

fsync 函数是 fsync 系统调用的后端实现,该函数用于刷新待处理的数据。若该函数指针为 NULL,那么系统调用将会返回 -EINVAL

fasync 函数

1
int (*fasync) (int, struct file *, int);

fasync 函数用于通知设备其 FASYNC 标志发生了变化,即异步通知。

以上几个函数是 struct file_operations 结构体中比较常用的函数,也是比较简单的几个函数。后续使用到其他未提到的函数时,对其进行详细的说明。

file

除了 struct file_operations 结构体,还需对 struct file 结构体有所了解。该结构体定义在 linux/fs.h 中,原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;

/*
* Protects f_ep, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;

u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;

#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct hlist_head *f_ep;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err; /* for syncfs */
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

上面是在 5.15 版本内核中对 struct file 结构体的完整定义。以下是针对其常用的重要成员的说明:

f_inode

1
struct inode *f_inode;

该结构体指针指向了此 file 结构体指向的真正文件节点。

f_op

1
const struct file_operations	*f_op;

f_op 指针指向与文件相关联的操作。内核在执行 open 时对这个指针赋值,但是在任何需要的时候,可以对该指针重新赋值来重新关联文件的操作。在返回给调用者之后,新的操作方法就会立即生效。

f_mode

1
fmode_t			f_mode;

f_mode 用于标识文件的模式,在 fs.h 中,定义了以下宏定义来对文件的模式进行检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* file is open for reading */
#define FMODE_READ ((__force fmode_t)0x1)
/* file is open for writing */
#define FMODE_WRITE ((__force fmode_t)0x2)
/* file is seekable */
#define FMODE_LSEEK ((__force fmode_t)0x4)
/* file can be accessed using pread */
#define FMODE_PREAD ((__force fmode_t)0x8)
/* file can be accessed using pwrite */
#define FMODE_PWRITE ((__force fmode_t)0x10)
/* File is opened for execution with sys_execve / sys_uselib */
#define FMODE_EXEC ((__force fmode_t)0x20)
/* File is opened with O_NDELAY (only set for block devices) */
#define FMODE_NDELAY ((__force fmode_t)0x40)
/* File is opened with O_EXCL (only set for block devices) */
#define FMODE_EXCL ((__force fmode_t)0x80)
/* File is opened using open(.., 3, ..) and is writeable only for ioctls
(specialy hack for floppy.c) */
#define FMODE_WRITE_IOCTL ((__force fmode_t)0x100)
/* 32bit hashes as llseek() offset (for directories) */
#define FMODE_32BITHASH ((__force fmode_t)0x200)
/* 64bit hashes as llseek() offset (for directories) */
#define FMODE_64BITHASH ((__force fmode_t)0x400)

文件的模式主要有可读、可写、可执行等。系统在调用驱动程序的相关操作之前就已经对文件的模式进行了检查,对于没有权限的操作,将被内核拒绝执行。因此,驱动程序无需为此做出额外的检查。

举例来讲,用户程序以只读的形式打开了一个设备,那么当用户程序对其进行 write 操作时,系统会在调用设备驱动程序的 write 操作之前检查该文件的模式。内核发现用户程序对此文件没有写权限,因此内核将拒绝执行后续的操作,不会调用到驱动程序的 write 操作。

f_pos

1
loff_t			f_pos;

f_pos 记录文件当前的读写位置,在 file_operations 中的 llseek 函数中已经做出了详细的说明。

private_data

1
void			*private_data;

open 系统调用在调用驱动的 open 函数之前将 private_data 字段置为 NULL,驱动程序可讲该字段用于任何目的或简单的忽略该字段。该字段时跨系统调用时保存状态信息非常有用的资源。在后续的示例代码中,将使用该字段保存一些十分有用的信息。

字符设备驱动程序

下面将以一个虚拟字符设备驱动程序为例,来学习字符设备驱动程序的编写。

字符设备驱动程序结构

字符设备驱动程序的大致结构如下:

vshd

如上图所示,字符设备驱动程序主要分为四个部分:

虚拟字符设备硬件: vchd (virtual character hardware device)

虚拟字符设备:vcd (virtual character device)

虚拟字符设备操作:vcdops (virtual character device operations)

虚拟字符设备驱动:vcdd (virtual character device driver)

字符设备

虚拟字符硬件设备 vchd

首先,在内存中虚拟出一个简单的字符设备的硬件: vchd (virtual character hardware device) ,用一个结构体来表示:

1
2
3
4
5
6
7
8
9
/** 
* @brief vchd 虚拟字符硬件设备(virtual character hardware device)
* @brief buff : 字符设备硬件内存
* @brief size : 字符设备硬件内存大小
*/
typedef struct{
char *buff;
int size;
}vchd_t;

该结构体包含一个内存 ( buff )与保存内存大小的变量(size)。

虚拟字符设备 vcd

在前面的内容中讲过, Linux 系统中使用 struct cdev 结构体来表示字符设备。因此,为了能够访问到该设备,我们还需要定义 struct cdev 和与之相关的变量。

我们将虚拟的字符设备硬件进行一层抽象,定义成一个结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @brief vcd_t 字符设备(virtual character device)
* @brief dev_id 设备号
* @brief pdev 设备指针
* @brief pclass 设备类
* @brief vchd 虚拟字符设备硬件
*/
typedef struct{
dev_t dev_id;
struct cdev *pdev;
struct class *pclass;
vchd_t vchd;
}vcd_t;

设备驱动

有了字符设备,我们还需要为其编写驱动程序,这样才能够使得设备可以被访问。

驱动程序包含两个部分,一个部分是对设备的操作,也就是 file_operations;另一部分就是驱动程序本身,包括加载、卸载等函数。

设备操作 vcdops

设备操作主要完成打开、关闭、读写等功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static int vcd_open(struct inode *pinode, struct file *pfile){
// do something
}

static int vcd_close(struct inode *pinode, struct file *pfile){
// do something
}

loff_t vcd_llseek(struct file* pfile, loff_t offset, int whence){
// do something
}

static ssize_t vcd_read(struct file *pfile, char __user *buff, size_t len, loff_t *offset){
// do something
}

static ssize_t vcd_write(struct file *pfile, const char __user *buff, size_t len, loff_t *offset){
// do something
}

static const struct file_operations vcd_ops = {
.owner = THIS_MODULE,
.open = vcd_open,
.release = vcd_close,
.llseek = vcd_llseek,
.read = vcd_read,
.write = vcd_write
};

驱动程序 vcdd

1
2
3
4
5
6
7
8
9
10
static int __init vcdd_init(void){
// do something
}

static void __exit vcdd_exit(void){
// do something
}

module_init(vcdd_init);
module_exit(vcdd_exit);

字符设备驱动程序源码

新建 CharacterDeviceDrive.c 文件,并写入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/types.h>
#include <linux/slab.h>
#include <linux/moduleparam.h>


static int VCD_COUNT = 4; // 虚拟字符设备的个数,默认为 4
static char *VCD_NAME = "virtual_character_device"; // 虚拟字符设备的名称
static dev_t FIRST_CDEVICE_REGION = 0; // 申请的第一个设备号
static int FIRST_REGION_MAJOR = 0; // 申请的第一个设备号的主设备号
static int FIRST_REGION_MINOR = 0; // 申请的第一个设备号的从设备号
struct class *VCD_CLASS = NULL; // 设备的类


/**
* @brief vchd 虚拟字符硬件设备(virtual character hardware device)
* @brief buff : 字符设备硬件内存
* @brief size : 字符设备硬件内存大小
*/
typedef struct{
char *buff;
int size;
}vchd_t;

/**
* @brief vchd_init 初始化虚拟字符硬件设备
* @param pdev [in] : 需要初始化的虚拟字符硬件设备的指针
* @return return = 0 : 初始化成功
* return = -1 : 初始化失败
*/
int vchd_init(vchd_t *pvchd){
if (pvchd == NULL){
printk(KERN_ERR "virtual character hardware device pointer is NULL\n");
return -1;
}
// 为虚拟硬件设备申请内核内存空间(1024 字节)
pvchd->size = 1024;
pvchd->buff = kmalloc(pvchd->size, GFP_KERNEL);

if (pvchd->buff == NULL){
printk(KERN_ERR "alloc virtual character hardware device buff failed\n");
return -1;
}
return 0;
}

/**
* @brief vchd_release 释放虚拟字符硬件设备
* @param pdev [in] : 需要销毁的虚拟字符设备的指针
* @return
*/
void vchd_release(vchd_t *pvchd){
if (pvchd == NULL){
printk(KERN_ERR "virtual character hardware device pointer is NULL\n");
return ;
}
kfree(pvchd->buff);
pvchd->buff = NULL;
return ;
}

/**
* @brief vcd_t 字符设备(virtual character device)
* @brief dev_id 设备号
* @brief pdev 设备指针
* @brief pclass 设备类
* @brief vchd 虚拟字符设备硬件
*/
typedef struct{
dev_t dev_id;
struct cdev *pdev;
struct class *pclass;
vchd_t vchd;
}vcd_t;

static vcd_t *vcd_list = NULL; // vcd 设备数组

/**
* @brief 打开字符设备
* @param pinode [in] 设备文件 struct inode 结构体指针
* @param pfile [in] 设备文件 struct file 结构体指针
* @return int return = 0 : 打开成功
* return = -1 : 打开失败
*/
static int vcd_open(struct inode *pinode, struct file *pfile){
printk(KERN_INFO "----------------------------------------\n");
printk(KERN_INFO "%s:\n", __func__);

if (pfile == NULL){
printk(KERN_ERR "file pointer is NULL\n");
return -1;
}
if (vcd_list == NULL){
printk(KERN_ERR "virtual character device list is empty\n");
return -1;
}

printk(KERN_INFO "open the device whitch major is %d, and the minor is %d\n", imajor(pinode), iminor(pinode));

// 使用 struct file 结构体中的 private_data 字段保存打开的设备
pfile->private_data = (void *)&(vcd_list + iminor(pinode) - FIRST_REGION_MINOR)->vchd;

return 0;
}

/**
* @brief 关闭字符设备
*
* @param pinode [in] 设备文件 struct inode 结构体指针
* @param pfile [in] 设备文件 struct file 结构体指针
* @return int return = 0 : 关闭成功
* return = -1 : 关闭失败
*/
static int vcd_close(struct inode *pinode, struct file *pfile){
printk(KERN_INFO "----------------------------------------\n");
printk(KERN_INFO "%s:\n", __func__);

if (pfile == NULL){
printk(KERN_ERR "file pointer is NULL\n");
return -1;
}
if (vcd_list == NULL){
printk(KERN_ERR "virtual character device list is empty\n");
return -1;
}
printk(KERN_INFO "close the device whitch major is %d, and the minor is %d\n", imajor(pinode), iminor(pinode));

pfile->private_data = NULL;

return 0;
}

/**
* @brief 修改设备文件读写位置
*
* @param pfile [in] 设备文件 struct file 结构体指针
* @param offset [in] 偏移量
* @param whence [in] 偏移的参考位置 SEEK_SET :文件头;SEEK_CUR : 文件当前位置;SEEK_END : 文件尾;
* @return loff_t return >= 0 : 当前文件的读写位置
* return = -1 : 修改失败
*/
loff_t vcd_llseek(struct file* pfile, loff_t offset, int whence){
vchd_t *pvchd;
loff_t new_offset = 0;

printk(KERN_INFO "----------------------------------------\n");
printk(KERN_INFO "%s:\n", __func__);

if (pfile == NULL){
printk(KERN_INFO "the file pointer is NULL\n");
return -1;
}

printk(KERN_INFO "the offset before llseek is %lld\n", pfile->f_pos);

// 通过 struct file 结构体的 private_data 指针获取到要进行操作的设备
pvchd = (vchd_t *)pfile->private_data;

// 计算 struct file 结构体新的 f_pos 值
switch (whence)
{
case SEEK_SET:
new_offset = offset < pvchd->size ? offset : pvchd->size;
break;
case SEEK_CUR:
new_offset = pfile->f_pos + offset < pvchd->size ? pfile->f_pos + offset : pvchd->size;
break;
case SEEK_END:
new_offset = pfile->f_pos + offset < pvchd->size ? pfile->f_pos + offset : pvchd->size;
break;
default:
break;
}

if(new_offset < 0)
return -1;

// 更新 struct file 结构体的 f_pos 值
pfile->f_pos = new_offset;

printk(KERN_INFO "the offset after llseek is %lld\n", pfile->f_pos);

return new_offset;
}

/**
* @brief 读取设备文件内存
*
* @param pfile [in] 设备文件 struct file 结构体指针
* @param buff [in] 用户空间的缓冲区地址
* @param len [in] 读取的字节数
* @param offset [in] 设备文件当前的读写位置
* @return ssize_t 成功读取的字节数
*/
static ssize_t vcd_read(struct file *pfile, char __user *buff, size_t len, loff_t *offset){
vchd_t *pvchd;
int failed_length = 0;

printk(KERN_INFO "----------------------------------------\n");
printk(KERN_INFO "%s:\n", __func__);

if (pfile == NULL){
printk(KERN_INFO "the file pointer is NULL\n");
return -1;
}
if (buff == NULL){
printk(KERN_ERR "the user buff is NULL\n");
return -1;
}

printk(KERN_INFO "the offset before read is %lld\n", *offset);

// 通过 struct file 结构体的 private_data 指针获取到要进行操作的设备
pvchd = (vchd_t *)pfile->private_data;

// 计算读取的数据长度
len = len <= (pvchd->size - *offset) ? len : (pvchd->size - *offset);

// 使用 copy_to_user 函数将设备的内存(内核空间)拷贝到用户内存(用户空间)
failed_length = copy_to_user(buff, pvchd->buff + *offset, len);

// 更新 strucr file 的 offset
*offset += (len - failed_length);

printk(KERN_INFO "the offset after read is %lld\n", *offset);
printk(KERN_INFO "current buff : %s\n", pvchd->buff);
return len;
}

/**
* @brief 写入设备文件内存
*
* @param pfile [in] 设备文件 struct file 结构体指针
* @param buff [in] 用户空间的缓冲区地址
* @param len [in] 读取的字节数
* @param offset [in] 设备文件当前的读写位置
* @return ssize_t 成功写入的字节数
*/
static ssize_t vcd_write(struct file *pfile,const char __user *buff, size_t len, loff_t *offset){
vchd_t *pvchd;
int failed_length = 0;

printk(KERN_INFO "----------------------------------------\n");
printk(KERN_INFO "%s:\n", __func__);

if (pfile == NULL){
printk(KERN_INFO "the file pointer is NULL\n");
return -1;
}
if (buff == NULL){
printk(KERN_ERR "the user buff is NULL\n");
return -1;
}

printk(KERN_INFO "the offset before write is %lld\n", *offset);

// 通过 struct file 结构体的 private_data 指针获取到要进行操作的设备
pvchd = (vchd_t *)pfile->private_data;

// 计算读取的数据长度
len = len <= (pvchd->size - *offset) ? len : (pvchd->size - *offset);

// 使用 copy_from_user 将用户内存(用户空间)中的数据拷贝到设备内存(内核空间)
failed_length = copy_from_user(pvchd->buff + *offset, buff, len);

// 更新 strucr file 的 offset
*offset += (len - failed_length);

printk(KERN_INFO "the offset after write is %lld\n", *offset);
printk(KERN_INFO "current buff : %s\n", pvchd->buff);
return len;
}

/**
* @brief vcd_ops 设备操作结构体
* @brief owner : 字拥有者信息
* @brief open : 打开设备操作
* @brief release : 关闭设备操作
* @brief llseek : 修改文件位置指针偏移量操作
* @brief read : 读取设备内存操作
* @brief write : 写入设备内存操作
*/
static const struct file_operations vcd_ops = {
.owner = THIS_MODULE,
.open = vcd_open,
.release = vcd_close,
.llseek = vcd_llseek,
.read = vcd_read,
.write = vcd_write
};

/**
* @brief 初始化模块
*/
static int __init vcdd_init(void){

int ret = 0;
int i = 0;

printk(KERN_INFO "----------------------------------------\n");
printk(KERN_INFO "%s:\n", __func__);

// 初始化 vcd 虚拟硬件设备列表
vcd_list = kmalloc(VCD_COUNT * sizeof(vcd_t), GFP_KERNEL);
if (vcd_list == NULL){
printk(KERN_ERR "kmalloc vcd_list failed\n");
goto failed_kmalloc_vcd_list;
}

// 初始化 vchd 虚拟硬件设备
for (i = 0; i < VCD_COUNT; i++){
if (vchd_init(&vcd_list[i].vchd) != 0){
while(i--){
vchd_release(&vcd_list[i].vchd);
}
printk(KERN_ERR "init vchd failed\n");
goto failed_vchd_init;
}
}

// 申请设备号
ret = alloc_chrdev_region(&FIRST_CDEVICE_REGION, 0, VCD_COUNT, VCD_NAME);
if (ret != 0){
printk(KERN_ERR "alloc chrdev region failed\n");
goto failed_alloc_chrdev_region;
}
FIRST_REGION_MAJOR = MAJOR(FIRST_CDEVICE_REGION);
FIRST_REGION_MINOR = MINOR(FIRST_CDEVICE_REGION);
printk(KERN_INFO "first region major is : %d, first region minor is %d\n", FIRST_REGION_MAJOR, FIRST_REGION_MINOR);

// 为 vcd_list 设备列表中的设备分配设备号
for (i = 0; i < VCD_COUNT; i++){
vcd_list[i].dev_id = MKDEV(FIRST_REGION_MAJOR, FIRST_REGION_MINOR + i);
}

// 申请并初始化设备
for (i = 0; i < VCD_COUNT; i++){
vcd_list[i].pdev = cdev_alloc();
if (vcd_list[i].pdev == NULL){
printk(KERN_ERR "alloc cdev failed\n");
goto failed_cdev_alloc;
}
cdev_init(vcd_list[i].pdev, &vcd_ops);
}

// 将设备添加到系统
for (i = 0; i < VCD_COUNT; i++){
if (cdev_add(vcd_list[i].pdev, vcd_list[i].dev_id, 1) != 0){
while(i--){
cdev_del(vcd_list[i].pdev);
}
printk(KERN_ERR "cdev add failed\n");
goto failed_cdev_add;
}
}

// 创建类
VCD_CLASS = class_create(THIS_MODULE, VCD_NAME);
if (IS_ERR(VCD_CLASS)){
printk(KERN_INFO "create class failed\n");
goto failed_create_class;
}
for (i = 0; i < VCD_COUNT; i++){
vcd_list[i].pclass = VCD_CLASS;
}

// 创建设备节点
for (i = 0; i < VCD_COUNT; i++){
if (device_create(vcd_list[i].pclass, NULL, vcd_list[i].dev_id, NULL, "%s%d", VCD_NAME, MINOR(vcd_list[i].dev_id)) == NULL){
while (i--){
device_destroy(vcd_list[i].pclass, vcd_list[i].dev_id);
}
printk(KERN_ERR "device create failed\n");
goto failed_device_create;
}
}

printk(KERN_INFO "init sucessed!\n");
return 0;


failed_device_create:
class_destroy(VCD_CLASS);

failed_create_class:
for (i = 0; i < VCD_COUNT; i++){
cdev_del(vcd_list[i].pdev);
}

failed_cdev_add:

failed_cdev_alloc:
unregister_chrdev_region(FIRST_CDEVICE_REGION, VCD_COUNT);

failed_alloc_chrdev_region:

failed_vchd_init:
kfree(vcd_list);
vcd_list = NULL;

failed_kmalloc_vcd_list:

return ret;
}

/**
* @brief 退出模块
*/
static void __exit vcdd_exit(void){
int i = 0;

printk(KERN_INFO "----------------------------------------\n");
printk(KERN_INFO "%s:\n", __func__);

for (i = 0; i < VCD_COUNT; i++){
device_destroy(vcd_list[i].pclass, vcd_list[i].dev_id);
}
class_destroy(VCD_CLASS);
for (i = 0; i < VCD_COUNT; i++){
cdev_del(vcd_list[i].pdev);
}
unregister_chrdev_region(FIRST_CDEVICE_REGION, VCD_COUNT);
kfree(vcd_list);

printk(KERN_INFO "exit sucessed!\n");
}

module_init(vcdd_init);
module_exit(vcdd_exit);

MODULE_LICENSE("GPL");

源码详解

头文件

在代码的 1-8 行,引入了该设备驱动程序所使用到的头文件

字符设备驱动程序信息

在代码的 11- 16 行:

1
2
3
4
5
6
static int VCD_COUNT = 4;                               // 虚拟字符设备的个数,默认为 4
static char *VCD_NAME = "virtual_character_device"; // 虚拟字符设备的名称
static dev_t FIRST_CDEVICE_REGION = 0; // 申请的第一个设备号
static int FIRST_REGION_MAJOR = 0; // 申请的第一个设备号的主设备号
static int FIRST_REGION_MINOR = 0; // 申请的第一个设备号的从设备号
struct class *VCD_CLASS = NULL; // 设备的类

定义了一系列的设备驱动程序的信息变量,包括设备数量、设备名称、设备号、设备类信息。

其中,VCD_COUNT 变量保存了虚拟设备的数量,在这里,设备的数量默认为 4 个。

虚拟字符硬件设备

在代码的第 19 - 64 行,定义了虚拟硬件设备结构体 vchd_t ,并为其定了了初始化和释放函数。

字符设备

在代码的第 66 - 78 行,定义了字符设备结构体 vcd_tvcd_t 结构体是对 vchd_t 的一层封装。

为什么要定义这两个结构体呢?

因为目前我们并没有实际上的字符设备硬件,因此我们使用了软件来模拟硬件设备的方法。可以将 vchd_t 视作一个实际存在的硬件设备,然后使用一个结构体 vcd_t 对其进行封装。用户程序中所进行的操作的对象都是 vcd_t 对象,然后驱动程序再通过 vcd_t 对象来间接操作硬件设备,即 vchd_t 对象。

在代码的第 80 行,定义了一个数组变量 vcd_list 来保存字符设备列表,在样例代码中,会创建四个字符设备,并保存在 vcd_list 数组中。

vcd_vchd

文件操作

在代码的第 82 - 294 行中,定义了 vcd_openvcd_closevcd_llseekvcd_readvcd_write 五个文件操作函数,并保存在 vcd_ops 变量中。

值的注意的是 vcd_open 函数。因为样例代码默认创建了四个设备,并为每个设备都创建了对应的设备文件节点,用户程序可以通过访问不同的设备文件节点来访问到不同的设备。那么驱动程序内部是如何知道用户访问的是哪一个节点呢?

在结构体 vcd_t 中,包含有 dev_t 类型的变量 dev_id 用来保存该设备的设备号。在用户程序对设备进行访问时,驱动程序可以通过用户程序操作设备的设备号来确定用户程序访问的是哪一个设备。

用户程序对设备进行操作时,首先得调用 open 函数来打开一个设备,驱动程序调用 struct file_operationsopen 函数对其进行响应,驱动程序中的 open 函数原型如下:

1
2
// 驱动程序 open 函数
static int (*open)(struct inode *pinode, struct file *pfile)

用户程序成功调用 open 函数后,内核会创建并维护一个 struct file 结构体对象,直到用户程序使用 close 关闭该设备文件。

在驱动程序中,读写等操作都是传入该 struct file 结构体指针来进行的(详见 struct file_operations 中的函数定义),如:

1
2
3
4
// 驱动程序文件操作函数
ssize_t (*read) (struct file *pfile, char __user *buff, size_t length, loff_t *position);
ssize_t (*write) (struct file *pfile, char __user *buff, size_t length, loff_t *position);
loff_t (*llseek) (struct file *pfile, loff_t offset, int whence);

与之对应的,在用户程序中,用户程序在对一个设备进行操作时,首先调用用户程序的 open 函数:

1
2
// 用户程序 open 函数 
int open(const char *pathname, int flags);

该函数返回一个 int 型的变量作为文件号,在后续的读写等操作函数中作为参数进行传入,如:

1
2
// 用户程序 read 函数
ssize_t read (int __fd, void *__buf, size_t __nbytes);

回到最开始的问题,我们已经知道可以通过设备设备号来确定用户程序真正想要操作的设备是哪一个,那么该怎样获取到设备号呢?

在驱动程序中 struct file_operations 结构体的 open 函数:

1
static int (*open)(struct inode *pinode, struct file *pfile)

中有两个入参:pinodepfile,在第 4 章节中讲到过,可以通过:

1
2
unsigned int iminor(struct inode *inode);     // 获取次设备号
unsigned int imajor(struct inode *inode); // 获取主设备号

两个函数来从 pinode 变量中获取到设备号,从而确定目标设备是哪一个。

对于其他的文件操作函数如:

1
2
3
4
// 驱动程序文件操作函数
ssize_t (*read) (struct file *pfile, char __user *buff, size_t length, loff_t *position);
ssize_t (*write) (struct file *pfile, char __user *buff, size_t length, loff_t *position);
loff_t (*llseek) (struct file *pfile, loff_t offset, int whence);

并没有 struct inode 对象传入,在这些函数中该如何确定目标设备呢?

一种方法是在 struct file 结构体中,定义有 struct inode 指针类型的成员:f_inode

1
2
3
4
5
struct file {
struct inode *f_inode;
void *private_data;
···
} __randomize_layout

可以通过该成员获取到设备的设备号。

另一种在 open 函数中,通过是通过 pinode 获取到设备号,然后通过设备号获取到目标设备,再将其指针保存在 struct file 结构体中的成员变量 private_data 中。在其他的操作函数中,使用 private_data 变量来访问设备的指针。

样例代码中使用了第二种方法。

在代码的第 105 行:

1
pfile->private_data = (void *)&(vcd_list + iminor(pinode) - FIRST_REGION_MINOR)->vchd;

将该段语句展开的话,是以下内容:

1
2
3
4
5
6
// 获取目标设备的次设备号
int target_dev_minor = iminor(pinode);
// 根据次设备号的起始值,计算目标设备在 vcd_list 中的下标
int target_dev_index = target_dev_minor - iminor(pinode) - FIRST_REGION_MINOR;
// 将目标设备的 vchd 对象地址保存在 pfile 的 private_data 变量中
pfile->private_data = (void *)&vcd_list[target_dev_index].vchd;

在驱动程序的 readwrite 等函数中,再将 private_data 转换为 vchd 指针进行访问,如代码的第 161 行:

1
(vchd_t *) pvchd = (vchd_t *)pfile->private_data;

初始化

在代码的第 308 - 323 行,根据 VCD_COUNT 初始化了 4 个设备,并保存在 vcd_list 变量中。

代码的第 335 - 338 行,使用 alloc_chrdev_region 申请设备号并为 vcd_list 中的每一个设备设置了设备号。

代码的第 340 - 348 行,使用 cdev_allocvcd_list 中的设备进行初始化。

代码的第 350 - 359 行,使用 cdev_add 将设备添加到系统中。

代码的第 361 - 369 行,使用 class_create 创建设备类,并为设备的 pclass 字段赋值。

代码的第 371 - 380 行,使用 device_create 创建设备节点。

代码的第 386 - 405 行,对驱动程序初始化中遇到的错误进行处理。

退出

代码的第 410 - 430 行为驱动程序的退出函数,在该函数中对申请的资源进行释放。

用户程序程序源码

新建 main.c 文件并写入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

int main()
{
int fd;
char path[32] = "/dev/virtual_character_device0";
char buf0[32] = "hello";
char buf1[32] = "word";
char buf2[32] = {0};
int ret = 0;

fd = open(path, O_RDWR);
if (fd < 0){
printf("open failed fd = %d\n", fd);
return 0;
}

printf("opend\n");

write(fd, buf0, strlen(buf0));
write(fd, buf1, strlen(buf1));

lseek(fd, 0, SEEK_SET);
ret = read(fd, buf2, strlen(buf0) + strlen(buf1));
printf("%s\n", buf2);

close(fd);
printf("closed\n");
return 0;
}

源码详解

在代码的第 11 行,定义了 fd 变量用来保存文件 open 函数返回的文件描述符。

代码的第 12 行,定义了设备文件节点的名称(路径)。

代码的第 13 -15 行,定义了三个字符串(buf0、buf1、buf2),分别用来向设备写入和读取。

代码的第 18 行,调用 open 函数,并将文件描述符保存在 fd 变量中。

代码的第 26 - 27 行,向设备两次写入 buf0、buf1 中的内容,用来观察设备节点文件的 f_pos 的值的变化;

代码的第 29 行,使用 lseek 函数将 f_pos 的值修改为 0。

代码的 30 行,读取设备的内容到 buf2 中。

运行与测试

编译

使用我们之前所使用的 Makefile 文件对驱动程序进行编译:

1
2
3
4
5
6
7
8
9
10
11
ifneq ($(KERNELRELEASE),)
obj-m := CharacterDeviceDriver.o
else
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
endif
1
make

image-20230205124703751

使用 gcc 编译用户程序文件:

1
gcc main.c -o main

image-20230205124839441

运行

使用 insmod 将驱动程序加载进系统并使用 dmesg 查看系统信息:

1
2
sudo insmod CharacterDeviceDriver.ko
sudo dmesg

image-20230205125127141

可以看到,驱动程序成功初始化。

查看系统的设备列表:

1
ls /dev

截屏2023-02-05 12.54.35

可以看到,4 个设备已经被加载进系统。

查看四个设备的设备号等信息:

1
ll /dev

截屏2023-02-05 13.02.34

查看 /peoc/devices

1
cat /proc/devices 

截屏2023-02-05 13.00.45

运行用户程序并查看 dmesg 信息:

1
2
sudo ./main
sudo dmesg

image-20230205170904892

可以看到,第一次进入 vcd_write 函数时,传入的 offset 的值为 0,在 vcd_write 函数最后,根据写入设备的数据长度,修改 offset 的值后,在第二次进入 vcd_write 函数时,offset 的值变成了 5。这说明修改 offset 后,pfile->f_pos 的值确实被修改了。

第一次调用 vcd_write 函数后,设备的 buff 中的值为写入的 hello

第二次调用 vcd_write 函数后,设备的 buff 中的值为 helloword

在用户程序调用 lseek 后,pfile->f_pos 的值被修改为 0,因此在用户程序调用 read 函数时,是从设备的 buff 的起始读取的。

修改用户程序,去掉向设备写入的部分,只读取设备,看一下第二次运行用户程序时,设备里的内容。

修改用户程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

int main()
{
int fd;
char path[32] = "/dev/virtual_character_device0";
char buf0[32] = "hello";
char buf1[32] = "word";
char buf2[32] = {0};
int ret = 0;

fd = open(path, O_RDWR);
if (fd < 0){
printf("open failed fd = %d\n", fd);
return 0;
}

printf("opend\n");

lseek(fd, 0, SEEK_SET);
ret = read(fd, buf2, strlen(buf0) + strlen(buf1));
printf("%s\n", buf2);

close(fd);
printf("closed\n");
return 0;
}

清除 dmesg 信息,编译并运行:

1
2
3
4
sudo dmesg -C
gcc main.c -o main
sudo ./main
sudo dmesg

image-20230206092919079

可以看到,第二次打开设备并读取其内容,第一次程序写入的数据仍然在设备中。

若要打开并操作其他的设备,只要将用户程序中的设备节点的路径设为 /dev 目录下对应的节点名称即可。

截屏2023-02-05 12.54.35

-------------本文结束 感谢阅读-------------
打赏一包辣条