浅谈linux - 字符设备框架

概述

linux系统将设备分为了字符设备、块设备和网络设备三大类。

字符设备是指在I/O传输过程中以字符为单位进行传输的设备,字符设备是面向流的设备,占linux设备驱动的绝大部分,常见的字符设备有鼠标、键盘、LED等。

注意

1、之前的文章已经提到过,在linux下,一切皆文件,字符设备当然也不例外,linux将字符设备抽象成设备文件,。因此用户操作硬件,实则是操作设备文件。

2、目标机中dev目录下存放着所有加载的设备文件,执行命令

ls /dev/dev_feng -lh,如下图所示,可以看到设备文件dev_feng 的具体属性。

/ # ls /dev/dev_feng -l
crw-rw----    1 0        0         249,   0 Mar  9 06:05 /dev/dev_feng

c:表示此文件为字符设备文件

249:表示此字符设备文件包含的主设备号

0:表示字符设备文件的次设备号

dev_feng :表示字符设备文件名

3、每个设备驱动文件在创建之前都必须向内核申请设备号,设备号用来唯一标识当前设备,由主设备号和次设备号两部分组成,数据类型为dev_t(unsigned int)。

主设备号:设备号的高12bit位,用于标明驱动,一个驱动仅有唯一的主设备号。

次设备号:设备号的低20bit位,用于标明硬件,一个硬件个体仅有唯一的次设备号。

linux内核提供设备号、主设备号、次设备号相互转换的宏。

/* 设备号、主设备号、次设备号相互转换的宏, 在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))

接口

1、在linux中字符设备通过结构体struct cdev表示。

/* linux内核描述字符设备的数据结构, 在linux/cdev.h中 */
struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;  /* 操作接口 */
    struct list_head list;              /* 内核链表 */
    dev_t dev;                          /* 设备号 */
    unsigned int count;                 /* 次设备号个数 */
};

2、应用和驱动接口的调用关系:

应用程序调用open/.../close->C库调用open/.../close->软中断->内核调用sys_open/.../sys_close->驱动调用open/.../close接口->应用open/.../close返回

3、linux内核用结构体struct file_operations来描述字符设备驱动的硬件操作接口数据结构, 在linux/fs.h中定义。

/* linux内核描述字符设备驱动的硬件操作接口数据结构, 在linux/fs.h中 */
struct file_operations {
    struct module *owner;
    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);        /* ioctl */
    int (*open) (struct inode *, struct file *);                                /* 打开 */                 
    int (*release) (struct inode *, struct file *);                             /* 关闭 */          ...
};

4、字符设备的注册(register_chrdev)和卸载(unregister_chrdev),一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行 。

/**
 * @向内核注册字符设备
 * @major: 主设备号,当用户设置为0时,内核会动态分配一个设备号
 * @name: 字符名,使用cat /proc/devices查看
 * @fops: 与此设备相关联的文件操作
 */
static inline int register_chrdev(unsigned int major, const char *name,
                  const struct file_operations *fops);

/**
 * @向内核卸载字符设备
 * @major: 主设备号
 * @name: 字符名
 */                  
static inline void unregister_chrdev(unsigned int major, const char *name);

5、创建字符设备节点文件。

a)、手动方式创建,使用mknode命令。该方法相对比较繁琐,且存在以下缺点:

①. 加载完驱动后,如果驱动加载顺序改变,必须重新通过cat /proc/devices命令获取申请的主设备号

②. 每次系统重启,要手动mknod创建。

/**
 * @创建字符设备节点    mknod:创建节点命令
 * @chr_name: 节点名字  c: 表明字符设备
 * @244:主设备号       0:次设备号
 */    
mknod /dev/chr_name c 244 0     chr_name - 字符设备名字

b)、自动方式创建。

①. 前提:自动创建方式使用busybox的mdev命令实现,所以必须保证根文件系统包含mdev命令,且在/etc/init.d/rcS 文件中有如下语句:echo /sbin/mdev > /proc/sys/kernel/hotplug。

②. 创建类:在创建设备节点之前必须先创建类,类的创建和销毁通过函数class_create和class_destroy完成,函数在linux/device.h中定义。

/**
 * @创建类
 * @owner: 一般为THIS_MODULE
 * @name: 类名,后期通过cd /sys/class查看
 * @返回创建的类
 */
extern struct class * __must_check __class_create(struct module *owner,
                          const char *name,
                          struct lock_class_key *key);
#define class_create(owner, name)        \
({                        \
    static struct lock_class_key __key; \
    __class_create(owner, name, &__key);    \
})

/**
 * @销毁类
 * @cls: 销毁的类
 */
void class_destroy(struct class *cls);

③. 创建设备节点:通过device_create函数创建,device_destroy函数销毁。

/**
 * @创建设备节点
 * @class: 类       parent:父设备,一般为NULL      devt:设备号
 * @drvdata:设备可能会使用的一些数据,一般为 NULL  
 * @fmt: 设备名,后期通过cd /sys/class/xxx/下查找
 * @返回创建好的设备
 */
struct device *device_create(struct class *class, struct device *parent,
                 dev_t devt, void *drvdata, const char *fmt, ...);

/**
 * @销毁设备节点
 * @class: 类       devt:设备号
 */         
void device_destroy(struct class *class, dev_t devt);

示例

★一直以来,C语言一直被定义为面向过程语言,主要是因为其缺少一些面向对象的语法(class类),但是我们在构建大型程序的时候一定要具备面向对象的思想来构建。 

C语言实现面向对象的思路大多是通过结构体和函数指针的方式,本示例中将字符设备处理单独抽象为一个模块(chrdev.c/chrdev.h),使用结构体struct class_chrdev定义为类名。

属性:设备号、设备名、类、设备节点。

行为:无。

★字符设备模块feng_chrdev,通过调用chrdev模块,创建字符设备,并实现了设备的打开、关闭、读写等操作。

★应用程序模块main,用户通过对字符设备文件操作映射到设备驱动,从而实现对硬件的操作。注意,在main和驱动层之间应该再多一层,用于封装设备节点相关操作接口,不过考虑到篇幅,所以此处省略了,直接在main中操作。

★包含头文件chrdev.h和源文件chrdev.c、feng_chrdev.c、main.c和编译规则文件Makefile(均已验证通过)。

chrdev.h

/**
 * @Filename : chr_dev.h
 * @Revision : $Revision: 1.00 $
 * @Author : Feng(更多编程相关的知识和源码见微信公众号:不只会拍照的程序猿,欢迎订阅)
 * @Description : 字符设备驱动框架类定义
**/

#ifndef __CHR_DEV_H__
#define __CHR_DEV_H__

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <linux/string.h> 


/* 设备类资源定义 */
struct class_chrdev {
    unsigned int major;     /* 记录设备号 */
    char chr_name[20];      /* 字符设备名 */
    struct class *cls;      /* 记录类 */
    struct device *dev;     /* 记录设备节点 */
};

/**
 * 创建字符设备对象,成功返回类地址,失败返回NULL
 * @cls_name: 类名,使用cd /sys/class查看
 * @chr_name: 字符名,使用cat /proc/devices查看
 * @dev_name: 设备名,使用cd /sys/class/xxx/下查找
 * @fops: file operations
 */
struct class_chrdev *chrdev_create(char *cls_name, char *chr_name, 
                                char *dev_name, struct file_operations *fops);

/**
 * 销毁字符设备对象
 * @chrdev: 字符设备对象
 */
void chrdev_destroy(struct class_chrdev *chrdev);

#endif

chrdev.c

/**
 * @Filename : chr_dev.c
 * @Revision : $Revision: 1.00 $
 * @Author : Feng(更多编程相关的知识和源码见微信公众号:不只会拍照的程序猿,欢迎订阅)
 * @Description : 字符设备驱动框架类定义
**/
#include "chrdev.h"

/**
 * 创建字符设备对象,成功返回类地址,失败返回NULL
 * @cls_name: 类名,使用cd /sys/class查看
 * @chr_name: 字符名,使用cat /proc/devices查看
 * @dev_name: 设备名,使用cd /sys/class/xxx/下查找
 * @fops: file operations
 */
struct class_chrdev *chrdev_create(char *cls_name, char *chr_name, 
                                char *dev_name, struct file_operations *fops)
{
    struct class_chrdev *chrdev = NULL;

    if ((chrdev = kmalloc(sizeof(struct class_chrdev), GFP_KERNEL)) == NULL) {
        printk(KERN_ERR "Allocation of port list failed\n");
        return NULL;
    }

    /* 创建设备号,动态分配 */
    if ((chrdev->major = register_chrdev(0, chr_name, fops)) > 0)
        printk("register chrdev ok...\n");
    else
        printk("register chrdev error...\n");

    strcpy(chrdev->chr_name, chr_name);

    /* 创建类和节点 */
    chrdev->cls = class_create(THIS_MODULE, cls_name);
    chrdev->dev = device_create(chrdev->cls, NULL, MKDEV(chrdev->major, 0), NULL, dev_name);

    return chrdev;
}

/**
 * 销毁字符设备对象
 * @chrdev: 字符设备对象
 */
void chrdev_destroy(struct class_chrdev *chrdev)
{
    device_destroy(chrdev->cls, MKDEV(chrdev->major, 0));
    class_destroy(chrdev->cls);
    unregister_chrdev(chrdev->major, chrdev->chr_name);
    kfree(chrdev);
}

EXPORT_SYMBOL_GPL(chrdev_create);
EXPORT_SYMBOL_GPL(chrdev_destroy);

MODULE_LICENSE("GPL");

MODULE_AUTHOR("feng");          /* 模块的作者 */
MODULE_VERSION ("1.00");        /* 模块版本号 */

feng_chrdev.c

/**
 * @Filename : feng_chrdev.c
 * @Revision : $Revision: 1.00 $
 * @Author : Feng(更多编程相关的知识和源码见微信公众号:不只会拍照的程序猿,欢迎订阅)
 * @Description : 字符设备驱动框架示例
**/

#include "chrdev.h"

struct class_chrdev *feng_chr;  /* 字符设备对象定义 */

/**
 * 打开接口实现
 */
static int feng_open(struct inode *inode, struct file *file);

/**
 * 关闭接口实现
 */
static int feng_close(struct inode *inode, struct file *file);

/**
 * 读取接口实现
 */
static ssize_t feng_read(struct file *file, char __user *user, size_t size, loff_t *off);

/**
 * 写入接口实现
 */
static ssize_t feng_write(struct file *file, const char __user *user, size_t size, loff_t *off);

/**
 * ioctl接口实现
 */
static long feng_ioctl(struct file *file, unsigned int cmd, unsigned long buf);

/* 字符设备驱动硬件操作接口 */
static struct file_operations feng_fops = {
    .read = feng_read,
    .write = feng_write,
    .unlocked_ioctl = feng_ioctl,
    .open = feng_open,
    .release = feng_close,
};

/**
 * 打开接口实现
 */
static int feng_open(struct inode *inode, struct file *file)
{
    printk("kernel:feng_open...\n---------------------------------\n");
    return 0;
}

/**
 * 关闭接口实现
 */
static int feng_close(struct inode *inode, struct file *file)
{
    printk("kernel:feng_close...\n---------------------------------\n");
    return 0;
}

/**
 * 读取接口实现
 */
static ssize_t feng_read(struct file *file, char __user *user, size_t size, loff_t *off)
{
    printk("kernel:feng_read...\n---------------------------------\n");
    return 0;
}

/**
 * 写入接口实现
 */
static ssize_t feng_write(struct file *file, const char __user *user, size_t size, loff_t *off)
{
    printk("kernel:feng_write...\n---------------------------------\n");
    return 0;
}

/**
 * ioctl接口实现
 */
static long feng_ioctl(struct file *file, unsigned int cmd, unsigned long buf)
{
    printk("kernel:feng_ioctl...\n---------------------------------\n");
    return 0;
}

/* 驱动模块入口函数 */
static int __init feng_drv_init(void)
{
    /* 注册设备 */
    if ((feng_chr = chrdev_create("cls_feng", "chr_feng", "dev_feng", &feng_fops)) == NULL) {
        printk("create feng char device error...\n");
        return -1;
    }

    return 0;
}

/* 驱动模块出口函数 */
static void __exit feng_drv_exit(void)
{
    chrdev_destroy(feng_chr);   /* 释放设备 */
}

module_init(feng_drv_init);
module_exit(feng_drv_exit);

MODULE_LICENSE("GPL");

MODULE_AUTHOR("feng");          /* 模块的作者 */
MODULE_VERSION ("1.00");        /* 模块版本号 */

main.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>

#define CHR_DEV_FILE    "/dev/dev_feng"     /* 设备驱动文件名 */

int main(int argc, char *argv[])
{
    /* 打开文件 */
    int index, ret;
    int fd = 0;

    /* 测试open */
    printf("app:test open...\n");
    if ((fd = open(CHR_DEV_FILE, O_RDWR)) < 0) {
        printf("app:%s file open failed...\n", CHR_DEV_FILE);
        return -1;
    } 

    /* 测试read */
    printf("app:test read...\n");
    ret = read(fd, &index, sizeof(index));

    /* 测试write */
    printf("app:test write...\n");
    ret = write(fd, &index, sizeof(index));

    /* 测试ioctl */
    printf("app:test ioctl...\n");
    ioctl(fd, 10, &index);

    /* 关闭文件 */
    printf("app:test close...\n");
    close(fd);

    return 0;
}

Makefile

#根文件所在目录
ROOTFS_DIR = /home/feng/atomic/rootfs

#交叉编译工具链
CROSS_COMPILE = arm-linux-gnueabihf-
CC = $(CROSS_COMPILE)gcc

#目标文件名
TAR_NAME = feng

#应用程序名字
APP_NAME = chr_test

#驱动目录路径
DRV_DIR = $(ROOTFS_DIR)/home/drv
DRV_DIR_LIB = $(ROOTFS_DIR)/lib/modules/4.1.15

#动态库目录路径
LIB_DIR = $(ROOTFS_DIR)/home/lib

#应用程序目录路径
APP_DIR = $(ROOTFS_DIR)/home/app

#KERNELRELEASE由内核makefile赋值
ifeq ($(KERNELRELEASE), )

#内核路径
KERNEL_DIR =/home/feng/atomic/resource/linux-imx-rel_imx_4.1.15_2.1.0_ga

#当前文件路径
CURR_DIR = $(shell pwd)

all:
    #编译模块
    make -C $(KERNEL_DIR) M=$(CURR_DIR) modules

    #编译应用程序
    -$(CC) -o $(APP_NAME) main.c

clean:
    #清除模块文件
    make -C $(KERNEL_DIR) M=$(CURR_DIR) clean

    #清除应用文件
    -rm $(APP_NAME)

install:
    #拷贝模块文件
    #cp -raf $(TAR_KEY_NAME)_drv.ko $(TAR_KEY_NAME)_dev.ko $(DRV_DIR)
    #cp -raf keyin.ko wq.ko timer.ko $(DRV_DIR_LIB)
    cp -raf *.ko $(DRV_DIR_LIB)

    #拷贝应用文件
    -cp -raf $(APP_NAME) $(APP_DIR)
else
#指定编译什么文件
obj-m += $(TAR_NAME)_chrdrv.o chrdev.o
#obj-m += $(TAR_NAME).o 

endif

结论

1、进入目录,执行make命令编译模块;然后执行make install命令,拷贝模块到目标机指定目录。

feng:chr_dev$ make
#编译模块
make -C /home/feng/atomic/resource/linux-imx-rel_imx_4.1.15_2.1.0_ga M=/mnt/hgfs/Share/linux/atomic/driver/chr_dev modules
make[1]: 进入目录“/home/feng/atomic/resource/linux-imx-rel_imx_4.1.15_2.1.0_ga”
  Building modules, stage 2.
  MODPOST 2 modules
make[1]: 离开目录“/home/feng/atomic/resource/linux-imx-rel_imx_4.1.15_2.1.0_ga”
#编译应用程序
arm-linux-gnueabihf-gcc -o chr_test main.c
feng:chr_dev$ make install 
#拷贝模块文件
#cp -raf _drv.ko _dev.ko /home/feng/atomic/rootfs/home/drv
#cp -raf keyin.ko wq.ko timer.ko /home/feng/atomic/rootfs/lib/modules/4.1.15
cp -raf *.ko /home/feng/atomic/rootfs/lib/modules/4.1.15
#拷贝应用文件
cp -raf chr_test /home/feng/atomic/rootfs/home/app
feng:chr_dev$ 

2、在目标机上执行modprobe命令加载模块。

注意:在模块加载之前,需要先调用depmod命令,生成模块依赖文件。

/sys/class/cls_feng # depmod
/sys/class/cls_feng # modprobe feng_chrdrv
register chrdev ok...
/sys/class/cls_feng # 

3、使用ls /sys/class命令查看是否存在cls_feng类。

/ # ls /sys/class  
ata_device     firmware       mem             rc               thermal
ata_link       gpio           misc            regulator        tty
ata_port       graphics       mmc_host        rfkill           ubi
backlight      i2c-dev        mtd             rtc              udc
bdi            ieee80211      net             scsi_device      vc
block          input          power_supply    scsi_disk        video4linux
cls_feng       lcd            pps             scsi_host        vtconsole
dma            leds           ptp             sound            watchdog
drm            mdio_bus       pwm             spi_master

4、在目标机上执行cat /proc/devices命令查看是否存在chr_feng设备。

/ # cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 29 fb
 81 video4linux
 89 i2c
 90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
207 ttymxc
226 drm
249 chr_feng
250 ttyLP
251 watchdog
252 ptp
253 pps
254 rtc

Block devices:
  1 ramdisk
259 blkext
  7 loop
  8 sd
 31 mtdblock
 65 sd
 66 sd
 67 sd
 68 sd
 69 sd
 70 sd
 71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
/ # 

5、在目标机上执行ls /sys/class/cls_feng/或者ls /dev/命令查看是否存在dev_feng设备文件。

/ # ls /sys/class/cls_feng/
dev_feng
/ # 
/ # ls /dev/
autofs                        ram14               tty37
bus                           ram15               tty38
console                       ram2                tty39
cpu_dma_latency               ram3                tty4
dev_feng                      ram4                tty40
dri                           ram5                tty41
fb0                           ram6                tty42
full                          ram7                tty43
fuse                          ram8                tty44
hwrng                         ram9                tty45
i2c-0                         random              tty46
i2c-1                         rfkill              tty47
input                         rtc0                tty48
kmsg                          snd                 tty49
loop-control                  tty                 tty5
loop0                         tty0                tty50
loop1                         tty1                tty51
loop2                         tty10               tty52
loop3                         tty11               tty53
loop4                         tty12               tty54
loop5                         tty13               tty55
loop6                         tty14               tty56
loop7                         tty15               tty57
mem                           tty16               tty58
memory_bandwidth              tty17               tty59
mmcblk1                       tty18               tty6
mmcblk1boot0                  tty19               tty60
mmcblk1boot1                  tty2                tty61
mmcblk1p1                     tty20               tty62
mmcblk1p2                     tty21               tty63
mmcblk1rpmb                   tty22               tty7
network_latency               tty23               tty8
network_throughput            tty24               tty9
null                          tty25               ttymxc0
pps0                          tty26               ttymxc1
pps1                          tty27               ubi_ctrl
ptmx                          tty28               urandom
ptp0                          tty29               vcs
ptp1                          tty3                vcs1
pts                           tty30               vcsa
ram0                          tty31               vcsa1
ram1                          tty32               video0
ram10                         tty33               watchdog
ram11                         tty34               watchdog0
ram12                         tty35               zero
ram13                         tty36
/ # 

6、在目标机上运行应用测试程序,查看是否正确调用驱动相应的接口信息。

/ # /home/app/chr_test 
app:test open...kernel:feng_open...
---------------------------------

app:test read...kernel:feng_read...
---------------------------------

app:test write...kernel:feng_write...
---------------------------------

app:test ioctl...kernel:feng_ioctl...
---------------------------------

app:test close...kernel:feng_close...
---------------------------------

/ # 

7、在目标机上执行modprobe -r命令卸载模块。

/ # modprobe -r feng_chrdrv
/ # lsmod
Module                  Size  Used by    Tainted: G  
/ # 

8、综上、示例展示了字符设备驱动框架,linux系统将字符设备驱动抽象为设备文件,应用程序使用open/close/read/write等接口操作设备文件,最终映射到驱动相当于调用驱动的open/release/read/write接口,所以对于驱动而言,最主要就是实现这些文件操作接口函数。

往期 · 推荐

帮你自动化办公的python-自动提取pdf指定页(文件处理篇)

帮你自动化办公的python-自动提取pdf指定页(项目概述)

也没想象中那么神秘的数据结构-一种通用化的双向链表设计(底层源码)

也没想象中那么神秘的数据结构-一环扣一环的“链表”(双向链表)

我用C语言玩对象,偷偷关注着你的观察者模式(基类设计)

关注

更多精彩内容,请关注微信公众号:不只会拍照的程序猿,本人致力分享linux、设计模式、C语言、嵌入式、编程相关知识,也会抽空分享些摄影相关内容,同样也分享大量摄影、编程相关视频和源码,另外你若想要本文章源码请关注公众号:不只会拍照的程序猿,后台回复:linux驱动源码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不只会拍照的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值