Linux 驱动常用函数

列举一些常用的函数

Posted by Jerry Chen on November 2, 2020

kzalloc

函数名 kzalloc
功能 申请空间并清零空间
正确返回值 空间地址
错误返回值 NULL
err.h 建议错误返回值 -ENOMEM
注销函数 kfree

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <linux/slab.h>
#include <linux/err.h>
gpio_dev_t *gpio_dev = NULL;

//申请空间
gpio_dev = kzalloc(sizeof(gpio_dev_t), GFP_KERNEL);
if(gpio_dev == NULL)
  return -ENOMEM;

//销毁空间(申请空间成功,创建设备类失败执行)
create_class_fail:
  kfree(gpio_dev);
  gpio_dev = NULL;

class_create

class 是设备的更高层视图,抽象出了底层的实现细节。驱动程序会区分 SCSI 硬盘和 ATA 硬盘,但在 class 层他们都是硬盘;

class_create 会动态创建设备的逻辑类,将其添加到内核中,成功创建后会在 /sys/class/ 下生成如 gpio_class 的目录;

函数名 class_create
功能 创建设备的逻辑类(自动申请空间)
正确返回值 struct class *
错误返回值 NULL
err.h 建议错误返回值 \
注销函数 class_destroy

示例:

1
2
3
4
5
6
7
8
9
10
11
#include <linux/device.h>
#include <linux/err.h>

//创建设备类
gpio_dev->class = class_create(THIS_MODULE, "gpio_class");
if (IS_ERR(gpio_dev->class))
  goto create_class_fail;

//销毁设备类(创建类成功,申请设备号失败执行)
region_chrdev_fail:
  class_destroy(gpio_dev->class);

alloc_chrdev_region

alloc_chrdev_region 会自动分配一段设备号,分配成功后会给 *dev 传参赋值第一个设备号,也会在 /proc/devices 文件中显示主设备号和设备名;

函数名 alloc_chrdev_region
功能 自动分配字符类设备号(主设备号分配未使用的)
传参 - dev_t *dev: 传入指针,返回第一个设备号
- unsigned baseminor: 次设备号起始递增
- unsigned count: 次设备号个数
- const char *name: 执行 cat /proc/devices 显示的名称
正确返回值 0
错误返回值 <0
err.h 建议错误返回值 \
注销函数 unregister_chrdev_region

示例:

1
2
3
4
5
6
7
8
9
10
#include <linux/fs.h>

//申请设备号
rev = alloc_chrdev_region(&gpio_dev->devt, 100/*次设备号起始*/, 8/*次设备号个数*/, "gpio_dev");
if (rev < 0)
  goto region_chrdev_fail;

//销毁设备号(申请设备号成功,添加 cdev 失败执行)
add_cdev_fail:
  unregister_chrdev_region(gpio_dev->devt/*第一个设备号*/, 8/*次设备号个数*/);

cdev 的使用:cdev_init

执行 cdev_init 函数,将 cdev 和 file_operations 关联起来,实际没有加入到内核;

函数名 cdev_init
功能 初始化字符设备,关联文件操作函数
传参 @ struct cdev *cdev: 将要初始化的 cdev
@ const struct file_operations *fops: 绑定的文件操作函数
返回值 void
注销函数 未加入到内核,无需注销

示例:

1
2
3
4
5
#include <linux/cdev.h>

//初始化 cdev
cdev_init(&gpio_dev->cdev, &gpio_fops); 
gpio_dev->cdev.owner = THIS_MODULE;

cdev 的使用:cdev_add

使用 cdev_add 函数,将 cdev 和设备号关联起来,即设备号和 fops 文件操作关联了起来,此时内核中就有 cdev 信息了;

接着可用 mknod 根据设备号手动创建设备节点,建立内核和用户系统的联系。

函数名 cdev_add
功能 关联 cdev 和设备号
传参 @ struct cdev *p: 传入 cdev
@ dev_t dev: 第一个设备号
@ unsigned count: 次设备号个数
正确返回值 0
错误返回值 非 0 值
err.h 建议错误返回值 \
注销函数 cdev_del

示例:

1
2
3
4
5
6
7
8
9
10
#include <linux/cdev.h>

//添加设备到 cdev 并加入内核,绑定了 devt 和 fops
rev = cdev_add(&gpio_dev->cdev, gpio_dev->devt, 8/*次设备号个数*/);
if(rev)
  goto add_cdev_fail;

//销毁内核中 cdev 的设备(添加到 cdev 成功,创建设备失败执行)
create_dev_fail:
  cdev_del(&gpio_dev->cdev);

device_create

device_create 函数会在 /dev目录下创建设备节点,建立内核和用户系统的联系。

函数名 device_create
功能 /dev 目录下创建设备节点
传参 @ class: 设备类
@ parent: 一般为 NULL
@ devt: 目标设备号
@ void *drvdata: 一般为 NULL
@ fmt: 可格式化的设备名
正确返回值 struct device *
错误返回值 NULL
err.h 建议错误返回值 \
注销函数 device_destroy

示例:

1
2
3
4
5
6
7
8
9
10
11
#include <linux/device.h>
#include <linux/kdev_t.h>

//创建设备节点(for 循环可创建多个)
gpio_dev->dev = device_create(gpio_dev->class, NULL, MKDEV(my_major, 0), NULL, "my_gpio" "%d", 0);
if (IS_ERR(gpio_dev->dev))
  goto create_dev_fail;

//销毁设备节点(创建设备节点成功,后续失败执行)
next_fail:
  device_destroy(&gpio_dev->class, MKDEV(my_major, 0));

用户空间和内核空间互访

用户空间传给内核的地址是虚拟地址,如果该地址未分配对应物理地址,就会触发内核缺页异常。copy_from_user 和 copy_to_user 会检查地址以及拷贝数据(类似 memcpy),当发生缺页后内核可分配物理地址并修改映射表解决该异常。

  1. 为什么要拷贝?

    答:直接传入虚拟地址使用的话,调度一段实际后,页表都变了,你这个 buf 地址还能找着人?

  2. 为什么不直接用 memcpy?

    答:如果进程之前使用过这个虚拟地址,那么该地址会分配对应的物理地址,短暂使用 memcpy 不会出现错误,调度一段时间或者该地址未分配物理地址就会出现内核缺页异常。

copy_from_user

1
long copy_from_user(void *to, const void __user * from, unsigned long n);

示例:

1
2
3
4
5
6
7
8
9
10
#include <asm/uaccess.h>

ssize_t mywrite(struct file *filp, const char __user *user_buf, size_t size, loff_t *offset)
{
    int ret = 0;
    struct test *pt = filp->private_data;	
    ret = copy_from_user(pt->kbuf, user_buf, size);
	
    return ret;
}

copy_to_user

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

示例:

1
2
3
4
5
6
7
8
9
10
#include <asm/uaccess.h>

ssize_t myread(struct file *filp, char __user *user_buf, size_t size, loff_t *offset)
{
    int ret = 0;
    struct test *pt = filp->private_data;	
    ret = copy_to_user(pt->kbuf, user_buf, size);
	
    return ret;
}

简单驱动示例

驱动层

demo.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <linux/ioctl.h>

#define KARG_SIZE	36
struct karg{
    int kval;
    char kbuf[KARG_SIZE];
};

#define CMDT		'A'
#define CMD_OFF		_IO(CMDT, 0) //命令 0 无方向 无参数
#define CMD_ON		_IO(CMDT, 1) //命令 1 无方向 无参数
#define CMD_R		_IOR(CMDT, 2, struct karg) //命令 2 方向读取 参数长度 sizeof(struct karg)
#define CMD_W		_IOW(CMDT, 3, struct karg) //命令 3 方向写入 参数长度 sizeof(struct karg)

demo.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
#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/err.h>
#include <asm/uaccess.h>
#include "demo.h"

/* 内核对象结构体 */
struct demo_obj{
    struct class *class;	//设备类
    dev_t devt;			//第一个设备号
    unsigned major;		//主设备号
    unsigned base_minor;	//第一个次设备号
    unsigned count;		//次设备号个数
    struct cdev cdev;		//cdev
};

/* 内核对象 */
struct demo_obj s_demo_obj = {
    .base_minor = 0,	//次设备号起始
    .count = 10,	//次设备号个数
};

/* 内核数据 */
static struct karg s_karg = {
    .kval = 0,
    .kbuf = {0},
};

static int dev_demo_open(struct inode *inode, struct file *file)
{
    return nonseekable_open(inode, file); //通知内核不支持 llseek
}

static int dev_demo_close(struct inode *inode, struct file *file)
{  
    return 0;
}

static long dev_demo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    /* cmd 为命令代码,arg 为命令参数或者数据的地址 */
    printk("ioctl: cmd=%ld arg=%ld\n", cmd, arg);

    switch(cmd){
    case CMD_ON:
        s_karg.kval = 1;
        break;
    case CMD_OFF:
        s_karg.kval = 0;
        break;
    case CMD_R:
        if(_IOC_SIZE(cmd) != sizeof(struct karg))
            return -EINVAL;

            if(copy_to_user((struct karg *)arg, &s_karg, sizeof(s_karg)))
                return -EAGAIN;
        break;
    case CMD_W:
        if(_IOC_SIZE(cmd) != sizeof(struct karg))
            return -EINVAL;

        if(copy_from_user(&s_karg, (struct karg *)arg, sizeof(s_karg)))
            return -EAGAIN;
        break;
    default: break;
    };
    return 0;
}

static const struct file_operations dev_demo_fops = { 
    .owner = THIS_MODULE,
    .open = dev_demo_open, //一定要有函数(函数里直接 return 也行),否则无法 open 设备节点
    .unlocked_ioctl = dev_demo_ioctl,
    .release = dev_demo_close, //同理,函数里直接 return 就行
};

static int __init demo_init(void)
{
    int i;
    printk("---demo_init\n");
    
    //创建类 同时创建了 /sys/class/demo_class/ 目录
    s_demo_obj.class = class_create(THIS_MODULE, "demo_class");
    
    //申请设备号 会加入到 /proc/devices 文件
    alloc_chrdev_region(&s_demo_obj.devt, s_demo_obj.base_minor/*次设备号起始*/, s_demo_obj.count/*次设备号个数*/, "demo_dev");
    s_demo_obj.major = MAJOR(s_demo_obj.devt);
    
    //初始化 cdev,绑定 fops
    cdev_init(&s_demo_obj.cdev, &dev_demo_fops); 
    s_demo_obj.cdev.owner = THIS_MODULE;
    
    //设备号的设备加入到 cdev,即和 fops 绑定
    cdev_add(&s_demo_obj.cdev, s_demo_obj.devt/*设备号起始*/, s_demo_obj.count/*次设备号个数*/);
    
    //打印一下设备号 比如 major=251 base_minor=0 count=10 可手动 mknod /dev/test/demo1 c 251 0 创建设备节点
    printk("major=%d base_minor=%d count=%d\n", s_demo_obj.major, s_demo_obj.base_minor, s_demo_obj.count);
    
    //以下通过 device_create 在 /dev/ 目录下创建设备节点,注意设备节点的权限
    for(i = 0; i < s_demo_obj.count; i++){
        device_create(s_demo_obj.class, NULL, MKDEV(s_demo_obj.major, s_demo_obj.base_minor +i), NULL, "demo" "%d", i);
    }
    return 0;
}

static void __exit demo_exit(void)
{
    int i;
    for(i = 0; i < s_demo_obj.count; i++){
        device_destroy(s_demo_obj.class, MKDEV(s_demo_obj.major, s_demo_obj.base_minor +i));
    }
    cdev_del(&s_demo_obj.cdev);
    unregister_chrdev_region(s_demo_obj.devt/*第一个设备号*/, s_demo_obj.count/*次设备号个数*/);
    class_destroy(s_demo_obj.class);
    printk("---demo_exit\n");
}

module_init(demo_init);
module_exit(demo_exit);
MODULE_DESCRIPTION("Demo Driver");
MODULE_LICENSE("GPL");

Makefile:

1
2
3
4
5
6
7
8
obj-m := demo.o
KDIR := /lib/modules/`uname -r`/build

all:
	make -C $(KDIR) M=$(PWD) modules
.PHONY : clean
clean :
	-rm -rf *.o *.ko .tmp_versions *.mod.c *.order *.symvers .demo*

应用层

test.h:(内容和 demo.h 除包含文件外相同)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/ioctl.h>

#define KARG_SIZE	36
struct karg{
    int kval;
    char kbuf[KARG_SIZE];
};

#define CMDT		'A'
#define CMD_OFF		_IO(CMDT, 0) //命令 0 无方向 无参数
#define CMD_ON		_IO(CMDT, 1) //命令 1 无方向 无参数
#define CMD_R		_IOR(CMDT, 2, struct karg) //命令 2 方向读取 参数长度 sizeof(struct karg)
#define CMD_W		_IOW(CMDT, 3, struct karg) //命令 3 方向写入 参数长度 sizeof(struct karg)

test.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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include "test.h"

static struct karg s_uarg = {
    .kval = 0,
    .kbuf = "test",
};

static struct karg s_uarg_r;

int main(int argc, char *argv[]){
    int fd;

    fd = open(argv[1], O_RDWR);
    if(fd < 0){
        printf("Cannot open %s\n", argv[1]);
        exit(1);
    }
    
    ioctl(fd, CMD_W, &s_uarg);		//写入参数:&s_uarg
    ioctl(fd, CMD_ON);			//无参数
    ioctl(fd, CMD_R, &s_uarg_r);	//读取返回到参数:&s_uarg_r
    
    printf("kval is %d\n", s_uarg_r.kval);
    printf("kbuf is %s\n", s_uarg_r.kbuf);
 
    close(fd);
    return 0;
}

结果展示

加载驱动后:

测试 /dev/demo0 节点,其他节点绑定的 fops 相同: