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即可

浙公网安备 33010602011771号