[課程筆記]Linux Driver正點原子課程筆記3 - 我的第一個Linux驅動

Posted by John on 2021-03-02
Words 2.5k and Reading Time 12 Minutes
Viewed Times

〖想觀看更多課程筆記,至[課程筆記]課程筆記系列總覽可以看到目前已發布的所有文章!〗

Course 3 - 我的第一個Linux驅動

字符設備驅動框架

上次有說到字符驅動提供給外部的api都被定義在file_operations structs內,這個結構被定義在/include/linux/fs.h下

0
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
/* /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 *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
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 (*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);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

簡單介紹一下這個struct內的member:

  • owner: 擁有該結構的module pointer,一般設置為THIS_MODULE
  • llseek(): 用於修改當前文件的讀寫位置
  • open(): 打開文件
  • read() & write():用來存取文件
  • poll(): 用來輪詢device是否可以進行non-blocking的讀寫
  • unlocked_ioctl() & compat_ioctl(): 提供對設備控制的街口
  • mmap(): 將device memory mapping到process memory,如此就不用做繁瑣的memory copy

function pointer不用全部實現,會用到哪個就實作哪個就好。

驅動模塊的加載與卸載

Linux驅動可以編譯到kernel裡面(zImage)。也可以編譯成module(.ko),需要的時候在載入就好,既然可以加載那就必須提供加載和卸載的func。

一個基礎的模板如下:

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <linux/module.h>
#include <linux/kernel.h>

/* LICENSE */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("john");


static int __init chrdevbase_init(void)
{
printk(KERN_INFO "module init\n");
return 0;
}
static void __exit chrdevbase_exit(void)
{
printk(KERN_INFO "module exit\n");
return;
}

module_init(chrdevbase_init); /* probe entry */
module_exit(chrdevbase_exit); /* exit entry */

kernel內沒有printf(),取而代之的是printk(),可以顯示不同level的log,log level的定義位於/include/linux/kern_levels.h

0
1
2
3
4
5
6
7
8
9
10
11
/* /include/linux/kern_levels.h */

#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */

#define KERN_DEFAULT KERN_SOH "d" /* the default kernel loglevel */

而在/include/linux/printk.h中定義了CONSOLE_LOGLEVEL_DEFAULT的預設值是7,也就是說優先級高於7的log才會被印出在console上

0
1
2
3
4
5
6
/* /include/linux/printk.h */
/*
* Default used to be hard-coded at 7, quiet used to be hardcoded at 4,
* we're now allowing both to be set from kernel config.
*/
#define CONSOLE_LOGLEVEL_DEFAULT CONFIG_CONSOLE_LOGLEVEL_DEFAULT
#define CONSOLE_LOGLEVEL_QUIET CONFIG_CONSOLE_LOGLEVEL_QUIET

module_init()是driver被載入時會執行的function,對於不同driver之間有時候是有著載入順序的相依性的,此時就可以透過呼叫不同的init function來控制,更深入的探討可以參考[Linux Kernel慢慢學]Linux modules載入及載入順序

而關於build code使用到了kbuild,一個Makefile的模板如下

KERNELDIR := /lib/modules/$(shell uname -r)/build
CURRENT_PATH := $(shell pwd)

obj-m := chrdevbase.o

build: kernel_modules

kernel_modules:
make -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
make -C $(KERNELDIR) M=$(CURRENT_PATH) clean
  • C表示切換到指定的目錄中
  • M表示模塊源碼目錄

編好後,可以透過ismod, modprobe, rmmod來加載和卸載,透過lsmod, modinfo來進行查看

  • ismod並不能解決module的依賴關係
  • modprobe會去/lib/modules/查看modules進行查找
    • 如果沒有該目錄,就自己創一個,然後將driver(.ko)放進去
    • modprobe <driver_name>.ko
    • 如果出現”can’t open ‘modules.dep’: no such file or directory”,可以透過depmod自動生成

字符設備的註冊與註銷

基於模塊加載&卸載的code繼續修改,註冊&註銷char device或使用到下列兩個function,被定義在/include/linux/fs.h中

0
1
2
3
4
5
/* /include/linux/fs.h */

static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)

static inline void unregister_chrdev(unsigned int major, const char *name)
  • marjor是主設備號,可以透過cat /proc/devices查看當前已註冊的的主設備號
  • name是device name
  • fops就是之前提到過的file_operations struct,也就是這個driver能提供哪些操作

設備號

為了方便管理,Linux下每個設備都有一個設備號,設備號由主設備號和次設備號構成

  • 前12bit為主設備號,表示某個具體的驅動
  • 後20bit為次設備號,表示使用該驅動的各個設備

Linux使用dev_t的type來表示設備號,可以看到它是一個unsigned int

0
1
2
3
/* /include/linux/types.h */
typedef u32 __kernel_dev_t;

typedef __kernel_dev_t dev_t;

關於主設備號、次設備號的定義位於include/linux/kdev_t.h

0
1
2
3
4
5
6
7
/* /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))

透過這幾個macro來產生設備號

register_chrdev()並沒有要填寫次設備號?他會佔用當前主設備號下的所有次設備號,比較不方便,所以有其他比較好用的register func,後面會提到

file_operations的具體實現

在字符設備驅動框架已經提過,我們必須要實作fops對應的function,然後將fops帶入註冊函數的參數中。

先寫個簡單的殼出來:

0
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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>

#define CHRDEVBASE_MAJOR 200
#define CHRDEVBASE_NAME "chrdevbase"

static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "chrdevbase_open\n");
return 0;
}

struct int chrdevbase_close(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "chrdevbase_close\n");
return 0;
}

struct int chrdevbase_read(struct file *filp, char *buffer, size_t length, loff_t offset)
{
printk(KERN_INFO "chrdevbase_read\n");
return 0;

}

struct int chrdevbase_write(struct file *filp, char *buf, size_t len, loff_t offset)
{
printk(KERN_INFO "chrdevbase_write\n");
return 0;
}

static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.close = chrdevbase_close,
.read = chrdevbase_read,
.write = chrdevbase_write,
};

static int __init chrdevbase_init(void)
{
int ret = 0;
printk(KERN_INFO "module init\n");
ret = register_chrdev(CHRDEVBASE_MAJOR, CHRDEBBASE_NAME, &chrdevbase_fops);
if (ret < 0) {
printk(KERN_INFO "chrdevbase init failed!\n");
}
}

static void __exit chrdevbase_exit(void)
{
printk(KERN_INFO "module init\n");
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEBBASE_NAME);
}

module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

關於struct中採用了”.(dot)”的成員變數宣告方式,可以參考[Linux Kernel慢慢學]探討Designated Initializers

應用程序編寫

Linux下一切皆文件,所以要操作driver也需要將該driver的字元文件打開,下面編寫一個簡易的應用程序來介紹如何開啟字元驅動文件

0
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
/**
* ./chrdevbaseApp <filename>
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
int fd = 0;
int ret = 0;
char *filename;
char readbuf[10], writebuf[100];

/* open() return a file descriptor */
fd = open(filename, O_RDWR);

if (fd == -1)
printf("Can not open file %s\n", filename);
return -1;

/* read */
ret = read(fd, readbuf, 50);
if (ret < 0)
printf("Read file fail\n");

/* write */
ret = write(fd, writebuf, 50);
if (ret < 0)
printf("Write file fail\n");

/* close */
ret = close(fd);
if (ret < 0)
printf("Close file failed\n");
return 0;
}

測試:

  1. 加載驅動: modbrpobe <driver_name>.ko
    • 加載完可透過cat /proc/devices確認
  2. 創建device node: mknod /dev/<driver_name> c 200 0
    • c代表字元驅動、200是主設備號、0是次設備號
    • 創建完後會在/dev下面看到驅動文件
  3. 透過應用開啟字元驅動文件: ./chrdevbaseAPP /dev/chrdevbase

chrdevbase虛擬設備驅動的完善

file_operations的具體實現應用程序編寫中只完成了驅動以及app的殼,這邊會將驅動和app剩下的code繼續完成。

驅動

  1. 驅動和數據的傳遞: data在kernel space和 user space不能直接調用,要透過copy_to_user(), copy_from_user()來進行轉換
    • #include <linux/uaccess.h>
0
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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define CHRDEVBASE_MAJOR 200
#define CHRDEVBASE_NAME "chrdevbase"

static char readbuf[100];
static char writebuf[100];
static char kerneldata[] = {"kernel data"};

static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "chrdevbase_open\n");
return 0;
}

struct int chrdevbase_close(struct inode *inode, struct file *filp)
{
printk(KERN_INFO "chrdevbase_close\n");
return 0;
}

struct int chrdevbase_read(struct file *filp, char *buf, size_t length, loff_t offset)
{
int ret = 0;
printk(KERN_INFO "chrdevbase_read\n");
memcpy(readbuf, kerneldata, sizeof(kerneldata));
ret = copy_to_user(buffer, readbuf, count);
if (ret == 0) {
printk("kernel send data: %s\n", readbuf);
} else {
/* error handle */
}
return 0;

}

struct int chrdevbase_write(struct file *filp, char *buf, size_t len, loff_t offset)
{
int ret = 0;
printk(KERN_INFO "chrdevbase_write\n");
ret = copy_from_user(writebuf, buf, count);
if (ret == 0) {
printk("kernel receive data: %s\n", writebuf);
} else {
/* error handle */
}

return 0;
}

static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.close = chrdevbase_close,
.read = chrdevbase_read,
.write = chrdevbase_write,
};

static int __init chrdevbase_init(void)
{
int ret = 0;
printk(KERN_INFO "module init\n");
ret = register_chrdev(CHRDEVBASE_MAJOR, CHRDEBBASE_NAME, &chrdevbase_fops);
if (ret < 0) {
printk(KERN_INFO "chrdevbase init failed!\n");
}
}

static void __exit chrdevbase_exit(void)
{
printk(KERN_INFO "module init\n");
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEBBASE_NAME);
}

module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

app

App要可以透過驅動對device做讀寫,open char device file後要可以做read/write

0
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
/**
* ./chrdevbaseApp <filename> <num>
* @num: 1 代表read, 2代表write
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
int fd = 0;
int ret = 0;
char *filename;
char readbuf[10], writebuf[100];
static char usrdata[] = {"usr data"};

if (argc != 3) return -1;

/* open() return a file descriptor */
fd = open(filename, O_RDWR);

if (fd == -1)
printf("Can not open file %s\n", filename);
return -1;

if (atoi(argv[2]) == 1 ) {
/* read */
ret = read(fd, readbuf, 50);
if (ret < 0)
printf("Read file fail\n");
else
printf("App read data: %s\n", readbuf);
}

if (atoi(argv[2]) == 2 ) {
memcpy(writebuf, usrdata, sizeof(usrdata));
/* write */
ret = write(fd, writebuf, 50);
if (ret < 0)
printf("Write file fail\n");
else
printf("Write data: %s \n", writebuf);
}

/* close */
ret = close(fd);
if (ret < 0)
printf("Close file failed\n");
return 0;
}

能夠透過app & driver之間做讀寫,代表未來就可以透過 app 去操控 driver 對 devices 做不同的操作(例如開關燈)


>