Linux .ko字符串驱动模块

Linux分为内核态和用户态

实则就是分为了用户操作空间和内核操作空间

Linux驱动开发分为两种,可以将驱动编译到内核kernel中即image,或者module中,即.ko文件,内核文件编译比较繁杂,通常编译到.ko文件中。

#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("You");
MODULE_DESCRIPTION("Simple test character device module");

static int __init mymodule_init(void){

    printk("mymodule_init\n");

    return 0;

}

static void __exit mymodule_exit(void){

    printk("mymodule_exit\n");

}
/*
*   模块的出口与入口函数
*/
module_init(mymodule_init);
module_exit(mymodule_exit);

这是一个最简单的字符串设备的驱动注册程序,这个代码的编写并没有难度,完全按照Linux官方的格式编写。难点在于这个程序的编译,Linux的编译多数使用Makefile文件,发展到今天已经形成了标准化的格式。对于模块驱动(.ko)的编译也不例外,使用标准格式即可。以下是Makefile文件的编写

KERNEL := /home/pro/prj/k230_linux_sdk/output/k230_canmv_lckfb_defconfig/build/linux-7d4e1f444f461dbe3833bd99a4640e7b6c2cd529
INC := /opt/toolchain/Xuantie-900-gcc-linux-6.6.0-glibc-x86_64-V3.0.2/include
CURRENT_PATH := $(shell pwd)
obj-m := mymodule.o
# Cross-compiler prefix (no trailing gcc) — used by kernel build system
CROSS_COMPILE := /opt/toolchain/Xuantie-900-gcc-linux-6.6.0-glibc-x86_64-V3.0.2/bin/riscv64-unknown-linux-gnu-
# Target architecture
ARCH ?= riscv

build: kernel_modules

kernel_modules:
    $(MAKE) -C $(KERNEL) M=$(CURRENT_PATH) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
    $(MAKE) -C $(KERNEL),$(INC) M=$(CURRENT_PATH) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean

字符串设备

注册设备我们需要使用一个函数register_chrdev,并且在卸载设备的时候也需要先注销一个注册的设备使用函数unregister_chrdev。
register_chrdev函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:

  • major:主设备号,Linux下每个设备都有一个设备号,设备号分为主设备号和次设备号两 部分,关于设备号后面会详细讲解。
  • name:设备名字,指向一串字符串。
  • fops:结构体file_operations类型指针,指向设备的操作函数集合变量。
    unregister_chrdev函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
  • major:要注销的设备对应的主设备号。
  • name:要注销的设备对应的设备名。
//设备号的原始类型,即一个无符号的32位整型
typedef u32 __kernel_dev_t;

typedef __kernel_fd_set     fd_set;
typedef __kernel_dev_t      dev_t;

Linux内核将设备号分为两类,主设备号和次设备号,故主设备号会占用高12位,次设备号占用低20位。

register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
unregister_chrdev(unsigned int major, const char *name);

这两个函数的注册方式在现在看来过于片面,可以在第一个参数中看出,在注册设备时只能填写major,但在一个设备注册时应该有主设备和次设备号互相作用的。这样就导致了注册到一个MAJOR时会直接忽略掉次设备号的全部字段, 2^12 = 4096个设备号被浪费。

const struct file_operations

这是Linux设备的属性结构体,其中定义了许多设备功能,就比如一个设备只有在注册的时候拥有open、close、read和write等等,这样在C语言中调用open函数这些函数时才有效。在内核文件Linux/include/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, struct io_comp_batch *,
            unsigned int flags);
    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 *);
    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);
    void (*splice_eof)(struct file *file);
    int (*setlease)(struct file *, int, 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);
    int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
    int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,
                unsigned int poll_flags);
} __randomize_layout;

字符设备驱动的函数实现

字符串操作函数实现

上面了解到一个字符串设备是否有功能,完全取决于动作结构体的实现。下面实现了四个基本操作功能,打开、读取、写入和关闭。这里的write和read,对于用户态调用时write是从用户写入到驱动,所以,以驱动为主体,write()函数应该叫做读取,read()叫做写入。即在显示read函数时应该将系统的数据写到缓存区,然后从缓存区写到用户缓存区,实现write函数时操作也是如此逻辑。

static int mydev_open(struct inode *inode, struct file *file) {
    // printk("mydev_open\n");
    return 0; // 成功打开设备
}

static int mydev_close (struct inode *inode, struct file *file) {
    // printk("mydev_close\n");
    return 0; // 成功关闭设备
}

static ssize_t mydev_read (struct file *file, char __user *user, size_t sizet, loff_t *loff_t){
    // printk("read succuee!");
    int ret = 0;
    memcpy(readbuf, kernel_buf, sizeof(kernel_buf));
    ret = copy_to_user(user, readbuf, sizet);
    return 0;
}

static ssize_t mydev_write (struct file *file, const char __user *user, size_t sizet, loff_t *loff_t){
    // printk("write succuee!");
    int ret  = 0;
    ret = copy_from_user(writebuf, user, sizet);
    // printk("writebuf:%s\r\n", writebuf);
    memcpy(kernel_buf, writebuf, sizet);
    return 0;

}

static struct file_operations mydev_fops = {
    // .owner = THIS_MODULE, // 这个字段在新版本内核中已被弃用
    // 其他文件操作函数可以在这里定义
    .owner = THIS_MODULE,
    .open = mydev_open,
    .release = mydev_close,//这里就是close()关闭功能
    .read = mydev_read,
    .write = mydev_write
};

makefile编译

下面是全部代码,结合以上提到的,我们编译即可得到.ko文件

KERNEL :=/home/pro/prj/k230_linux_sdk/output/k230_canmv_lckfb_defconfig/build/linux-7d4e1f444f461dbe3833bd99a4640e7b6c2cd529
INC := /opt/toolchain/Xuantie-900-gcc-linux-6.6.0-glibc-x86_64-V3.0.2/include
CURRENT_PATH := $(shell pwd)
obj-m := mymodule.o
# Cross-compiler prefix (no trailing gcc) — used by kernel build system
CROSS_COMPILE := /opt/toolchain/Xuantie-900-gcc-linux-6.6.0-glibc-x86_64-V3.0.2/bin/riscv64-unknown-linux-gnu-
# Target architecture
ARCH ?= riscv

build: kernel_modules

kernel_modules:
    $(MAKE) -C $(KERNEL) M=$(CURRENT_PATH) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
    $(MAKE) -C $(KERNEL) M=$(CURRENT_PATH) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean
make
#include "linux/printk.h"
#include <linux/module.h> //模块注册
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/types.h> //设备号文件
#include <linux/kdev_t.h> //设备号设置宏
#include <linux/fs.h>
#include <asm/string.h>
#define MYDEV_MAJOR 200
#define MYDEV_NAME "mydemkv"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("You");
MODULE_DESCRIPTION("Simple test character device module");

char readbuf[100] = {0};
char writebuf[100] = {0};
char kernel_buf[100] = "hello wolrd";

static int mydev_open(struct inode *inode, struct file *file) {
    // printk("mydev_open\n");
    return 0; // 成功打开设备
}

static int mydev_close (struct inode *inode, struct file *file) {
    // printk("mydev_close\n");
    return 0; // 成功关闭设备
}

static ssize_t mydev_read (struct file *file, char __user *user, size_t sizet, loff_t *loff_t){
    // printk("read succuee!");
    int ret = 0;
    memcpy(readbuf, kernel_buf, sizeof(kernel_buf));
    ret = copy_to_user(user, readbuf, sizet);
    return 0;
}

static ssize_t mydev_write (struct file *file, const char __user *user, size_t sizet, loff_t *loff_t){
    // printk("write succuee!");
    int ret  = 0;
    ret = copy_from_user(writebuf, user, sizet);
    // printk("writebuf:%s\r\n", writebuf);
    memcpy(kernel_buf, writebuf, sizet);
    return 0;

}

static struct file_operations mydev_fops = {
    // .owner = THIS_MODULE, // 这个字段在新版本内核中已被弃用
    // 其他文件操作函数可以在这里定义
    .owner = THIS_MODULE,
    .open = mydev_open,
    .release = mydev_close,
    .read = mydev_read,
    .write = mydev_write
};

static int __init mymodule_init(void){
    int ret = 0;
    ret = register_chrdev(MYDEV_MAJOR, MYDEV_NAME, &mydev_fops);
    // register_chrdev_region(dev_t, unsigned int, const char *);
    if(ret < 0){
        printk("Failed to register mydev device\n");
        return ret;
    }
    printk("mymodule_init\n");
    return 0;
}

static void __exit mymodule_exit(void){
    unregister_chrdev(MYDEV_MAJOR, MYDEV_NAME);
    printk("mymodule_exit\n");
}

/*
*   模块的出口与入口函数
*/
module_init(mymodule_init);
module_exit(mymodule_exit);

c代码测试

以下是测试代码

// #include "asm-generic/fcntl.h"
#include "stdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
/*
*   arg 应用程序的参数
*   chr 具体的参数内容,字符串形式
*   ./mydev <filename>or<filepath> <1:Read 2:Write> <string>
*/  

int main(int arg, char **chr){
    int ret = 0;
    int fd = 0;
    char buf[20];
    if(arg != 3){
        printf("please input /mydev <filename>or<filepath> <1:Read 2:Write> <string>\r\n");
        return -1;
    }
    fd = open(chr[1], O_RDWR);
    if(fd < 0){
        printf("open is failed %s\r\n", chr[1]);
        return -1;
    }

    //write 写入文件
    if(atoi(chr[2]) == 1){
        char writebuf[20] = {0};
        memcpy(writebuf, chr[3], sizeof(chr[3]));
        ret = write(fd, writebuf, 20);
        if(ret < 0){
            printf("%s is failed write\r\n", chr[1]);
            // memcpy(kernel_buf, writebuf, 20);
            return -1;
        }
        printf("write data :%s\r\n", writebuf);
        ret = close(fd);
        return 0;
    }

    //read 读取文件
    if(atoi(chr[2]) == 2){
        ret = read(fd, buf, 20);
    if(ret < 0){
        printf("%s is read failed\r\n", chr[1]);
            return -1;
    }else{
            printf("buf:%s\r\n", (char*)buf);
        }
    }
    ret = close(fd);
    return 0;
}
//使用make编译或者gcc都行,我使用的k230 Linux固件,需要使用xuantie-gcc编译

由于我们额外编译的模块(驱动)无法直接被内核加载,我们需要将.ko传到设备,然后通过指令加载到内核。

modprobe ./xxxx.ko
mknod /dev/mydev c 200 0 

主设备号对应注册时的编号,次设备号填0即可

待更新

posted @ 2026-01-01 17:36  混的牛/。  阅读(3)  评论(0)    收藏  举报