韦东山嵌入式Linux系列实验一:入门初探
1 前言
笔者使用的是韦东山STM32MP157 Pro的板子,环境搭建部分按照说明文档配置完成。配置桥接网卡实现板子、windows、ubuntu的通信,也在开发板挂载 Ubuntu 的NFS目录 ,这里就不再赘述了。
板子: 192.168.5.9
windows: 192.168.5.10
ubuntu: 192.168.5.11
在板子上执行
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs/ /mnt
2 开发板的第 1 APP 个实验
hello.c
/*************************************************************************
> File Name: hello.c
> Author: Winter
> Created Time: Sat 06 Jul 2024 04:44:00 AM EDT
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
int main(int argc, char* argv[])
{
if (argc >= 2)
printf("Hello, %s!\n", argv[1]);
else
printf("Hello, world!\n");
return 0;
}
在ubuntu上编译运行
这个程序不是能直接在开发板上运行的,需要使用arm版的工具链
在ubuntu上使用开发板的工具链重新编译,就可以在开发板上执行了
arm-buildroot-linux-gnueabihf-gcc hello.c
3 开发板的第 1 驱动实验
为什么编译驱动程序之前要先编译内核?
驱动程序要用到内核文件:内核/设备树/其他驱动程序
比如驱动程序中这样包含头文件: #include <asm/io.h>,其中的 asm 是一个链接文件,指向 asm-arm 或 asm-mips,这需要先配置、编译内核才会生成 asm 这个链接文件。
编译驱动时用的内核、开发板上运行到内核,要一致:放到板子上
开发板上运行到内核是出厂时烧录的,你编译驱动时用的内核是你自己编译的,这两个内核不一致时会导致一些问题。所以我们编译驱动程序前,要把自己编译出来到内核放到板子上去,替代原来的内核。
更换板子上的内核后,板子上的其他驱动也要更换:编译测试第一个驱动程序
板子使用新编译出来的内核时,板子上原来的其他驱动也要更换为新编译出来的。所以在编译我们自己的第 1 个驱动程序之前,要先编译内核、模块,并且放到板子上去
3.1 编译内核
不同的开发板对应不同的配置文件, 配置文件位于内核源码arch/arm/configs/目录。 kernel 的编译过程如下:
cd 100ask_stm32mp157_pro-sdk/Linux-5.4/
make 100ask_stm32mp157_pro_defconfig
编译内核
make uImage LOADADDR=0xC2000040 -j10
等待,结果如下
编译设备树
make dtbs
编译完成后, 在 arch/arm/boot 目录下生成 uImage 内核文件, 在arch/arm/boot/dts 目录下生成设备树的二进制文件 stm32mp157c-100ask-512d-v1.dtb。把这 2 个文件复制到/home/book/nfs_rootfs 目录下备用
cp arch/arm/boot/uImage ~/nfs_rootfs/
cp arch/arm/boot/dts/stm32mp157c-100ask-512d-v1.dtb ~/nfs_rootfs/
3.2 编译安装内核模块
进入内核源码目录后,就可以编译内核模块了:
cd /home/book/100ask_stm32mp157_pro-sdk/Linux-5.4
make ARCH=arm CROSS_COMPILE=arm-buildroot-linux-gnueabihf- modules -j10
内核模块编译完成后如图
安装内核模块到 Ubuntu 某个目录下备用
可以先把内核模块安装到 nfs 目录(/home/book/nfs_rootfs)。注意: 后面会使用 tree 命令查看目录结构, 如果提示没有该命令, 需要执行以下命令安装 tree 命令:
sudo apt install tree
把模块安装在 nfs 目录“ /home/book/nfs_rootfs/” 下
make ARCH=arm INSTALL_MOD_PATH=/home/book/nfs_rootfs INSTALL_MOD_STRIP=1 modules_install
安装好驱动后的/home/book/nfs_rootfs/目录结构如图
tree /home/book/nfs_rootfs/
3.3 安装内核和模块到开发板上
假设:在 Ubuntu 的/home/book/nfs_rootfs 目录下, 已经有了 zImage、dtb 文件,并且有 lib/modules 子目录(里面含有各种模块)。 接下来要把这些文件复制到开发板上。假设 Ubuntu IP 为 192.168.5.11,在开发板上执行以下命令:
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
mount /dev/mmcblk2p2 /boot
cp /mnt/uImage /boot # 内核
cp /mnt/*.dtb /boot # 设备树
cp /mnt/lib/modules /lib -rfd # 模块
sync
reboot
后面#是注释,不用粘上去
最后重启开发板,它就使用新的 zImage、 dtb、模块了
这里有个问题,图中标出来的地方,问题不大,参考:vmmcsd_fixed: disabling 自动弹出 – STM32MP157_PRO – 嵌入式开发问答社区
3.4 第一个驱动
怎么编写驱动程序
① 确定主设备号,也可以让内核分配
② 定义自己的 file_operations 结构体
③ 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
④ 把 file_operations 结构体告诉内核: register_chrdev
⑤ 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
⑥ 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
⑦ 其他完善:提供设备信息,自动创建设备节点: class_create,device_create
解释:需要实现驱动程序对应的open/write/read等函数,将这些函数放在file_operations 结构体里面,再将这个结构体注册到内核里面(register_chrdev函数),注册到什么地方呢,由主设备号区分(类似一个数组chrdevs[主设备号])。入口函数调用注册函数;有入口就有出口函数(卸载驱动程序)。
应用程序调用open函数打开一个文件"/dev/xxx",最终得到一个整数(文件描述符),这个整数对应内核中的一个结构体struct file
lag、mode就会保存在这个结构体的这两个参数中,还有一个f_op属性,里面有read/write/open等函数
应用程序打开某个设备节点时/dec/xxx,会根据设备节点的主设备号,在内核的chrdevs数组中,找到file_operation结构体,这个结构体中提供了驱动程序的read/write/open等函数。
参考 driver/char 中的程序,包含头文件,写框架,传输数据:
驱动中实现 open, read, write, release, APP 调用这些函数时,都打印内核信息
APP 调用 write 函数时,传入的数据保存在驱动中
APP 调用 read 函数时,把驱动中保存的数据返回给 APP
放到ubuntu的/home/book/nfs_rootf/01hello_drv下
hello_drv.c
主要还是围绕
① 确定主设备号,也可以让内核分配
② 定义自己的 file_operations 结构体
③ 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
④ 把 file_operations 结构体告诉内核: register_chrdev
⑤ 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
⑥ 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
⑦ 其他完善:提供设备信息,自动创建设备节点: class_create,device_create
/*************************************************************************
> File Name: hello.drv.c
> Author: Winter
> Created Time: Sun 07 Jul 2024 12:35:19 AM EDT
************************************************************************/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
// 1确定主设备号,也可以让内核分配
static int major = 0; // 让内核分配
static char kernel_buf[1024]; // 保存应用程序的数据
static struct class *hello_class;
#define MIN(a, b) (a < b ? a : b)
// 3 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
// 将kernel_buf区的数据拷贝到用户区数据buf中,即从内核kernel_buf中读数据
err = copy_to_user(buf, kernel_buf, MIN(1024, size));
return MIN(1024, size);
}
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
// 把用户区的数据buf拷贝到内核区kernel_buf,即向写到内核kernel_buf中写数据
err = copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
static int hello_drv_open (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
static int hello_drv_close (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
// 2定义自己的 file_operations 结构体
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
};
// 4把 file_operations 结构体告诉内核: register_chrdev
// 5谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
static int __init hello_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
// 注册hello_drv,返回主设备号
major = register_chrdev(0, "hello", &hello_drv); /* /dev/hello */
// 创建class
hello_class = class_create(THIS_MODULE, "hello_class");
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello");
return -1;
}
// 创建device
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
return 0;
}
// 6有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
static void __exit hello_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
// 卸载
unregister_chrdev(major, "hello");
}
// 7其他完善:提供设备信息,自动创建设备节点: class_create,device_create
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
测试程序:hello_drv_test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
* ./hello_drv_test -w abc
* ./hello_drv_test -r
*/
int main(int argc, char **argv)
{
int fd;
char buf[1024];
int len;
/* 1. 判断参数 */
if (argc < 2)
{
printf("Usage: %s -w <string>\n", argv[0]);
printf(" %s -r\n", argv[0]);
return -1;
}
/* 2. 打开文件 */
fd = open("/dev/hello", O_RDWR);
if (fd == -1)
{
printf("can not open file /dev/hello\n");
return -1;
}
/* 3. 写文件或读文件 */
if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
{
len = strlen(argv[2]) + 1;
len = len < 1024 ? len : 1024;
write(fd, argv[2], len);
}
else
{
len = read(fd, buf, 1024);
buf[1023] = '\0';
printf("APP read : %s\n", buf);
}
close(fd);
return 0;
}
Makefile:换成自己的内核
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_stm32mp157_pro-sdk/Linux-5.4
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
rm -f hello_drv_test
obj-m += hello_drv.o
编译
因为重新编译安装了内核,所以要在板子上重新挂载
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs/ /mnt
装载驱动程序
insmod hello_drv.ko
cat /proc/devices
lsmod
执行测试程序
4 Hello 驱动中的一些补充知识
4.1 module_init/module_exit 的实现
一个驱动程序有入口函数、出口函数,代码如下
module_init(hello_init);
module_exit(hello_exit);
驱动程序可以被编进内核里,也可以被编译为 ko 文件后手工加载。 对于这两种形式,“ module_init/module_exit”这 2 个宏是不一样的。 在内核文件“ include\linux\module.h”中可以看到这 2 个宏:
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
/**
* module_exit() - driver exit entry point
* @x: function to be run when driver is removed
*
* module_exit() will wrap the driver clean-up code
* with cleanup_module() when used with rmmod when
* the driver is a module. If the driver is statically
* compiled into the kernel, module_exit() has no effect.
* There can only be one per module.
*/
#define module_exit(x) __exitcall(x);
具体的
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
编译驱动程序时,我们执行“ make modules”这样的命令,它在编译 c 文件时会定义宏 MODULE,比如
arm-buildroot-linux-gnueabihf-gcc -DMODULE -c -o hello_drv.o hello_drv.c
在编译内核时,并不会定义宏 MODULE。所以, “module_init/module_exit”这 2 个宏在驱动程序被编进内核时,如下面代码中那样定义:
#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(x);
在驱动程序被编译为 ko 文件时,得到如下定义:
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
把上述代码里的宏全部展开后,得到如下代码:
#ifndef MODULE
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
/**
* module_exit() - driver exit entry point
* @x: function to be run when driver is removed
*
* module_exit() will wrap the driver clean-up code
* with cleanup_module() when used with rmmod when
* the driver is a module. If the driver is statically
* compiled into the kernel, module_exit() has no effect.
* There can only be one per module.
*/
#define module_exit(x) __exitcall(x);
// 深挖
#define __initcall(fn) __define_initcall("1", fn)
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
#define ___define_initcall(fn, id, __sec) \
__ADDRESSABLE(fn) \
asm(".section \"" #__sec ".init\", \"a\" \n" \
"__initcall_" #fn #id ": \n" \
".long " #fn " - . \n" \
".previous \n");
#else
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
#endif
#else /* MODULE */
// ...
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
#endif
驱 动 程 序 被 编 进 内 核 时 , 把 “ module_init(hello_init) ”、“ module_exit(hello_exit)”展开,得到如下代码,走的是else的判断
/* Each module must use one module_init(). */
#define module_init(hello_init) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return hello_init; } \
int init_module(void) __copy(hello_init) __attribute__((alias(#hello_init)));
/* This is only required if you want to be unloadable. */
#define module_exit(hello_init) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return hello_init; } \
void cleanup_module(void) __copy(hello_init) __attribute__((alias(#hello_init)));
其中的“ initcall_t”、“ exitcall_t”就是函数指针类型,所以上述代码就是定义了两个函数指针: 第 1 个函数指针名为__initcall_hello_init6,放 在 段 ".initcall6.init" 里 ; 第 2 个 函 数 指 针 名 为
__exitcall_hello_exit,放在段“.exitcall.exit”里。
内核启动时,会去段".initcall6.init"里取出这些函数指针来执行,所以驱动程序的入口函数就被执行了。一个驱动被编进内核后,它是不会被卸载的,所以段“ .exitcall.exit”不会被用到,内核启动后会释放这块段空间。
驱动程序被编译为ko文件时 , 把 “ module_init(hello_init) ”、“ module_exit(hello_exit)” 展开,得到如下代码:
static inline initcall_t __inittest(void) \
{ return hello_init; } \
int init_module(void) __attribute__((alias("hello_init")));
static inline exitcall_t __exittest(void) \
{ return hello_exit; } \
void cleanup_module(void) __attribute__((alias("hello_exit")));
分别定义了 2 个函数:第 1 个函数名为 init_module,它是 hello_init函数的别名;第 2 个函数名为 cleanup_module,它是 hello_exit 函数的别名。
以后我们使用 insmod 命令加载驱动时,内核都是调用 init_module 函数,实际上就是调用 hello_init 函数;使用 rmmod 命令卸载驱动时,内核都是调用 cleanup_module 函数,实际上就是调用 hello_exit 函数。
4.2 register_chrdev 的内部实现
register_chrdev 函数源码如下:
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
继续深挖
int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops)
{
struct char_device_struct *cd;
struct cdev *cdev;
int err = -ENOMEM;
// 注册字符设备的区域 ”, 它仅仅是查看设备号 (major, baseminor) 到 (major, baseminor+count-1)有没有被占用,
// 如果未被占用的话,就使用这块区域。
cd = __register_chrdev_region(major, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
// 分配
cdev = cdev_alloc();
if (!cdev)
goto out2;
// set数据
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
// add
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
if (err)
goto out;
cd->cdev = cdev;
return major ? 0 : cd->major;
out:
kobject_put(&cdev->kobj);
out2:
kfree(__unregister_chrdev_region(cd->major, baseminor, count));
return err;
}
在前面的课程里,在引入驱动程序时为了便于理解,我们说内核里有一个chrdevs 数组 , 根据主 设备号 major 在 chrdevs[major] 中放入file_operations 结构体,以后 open/read/write 某个设备文件时,就是根据主设备号从 chrdevs[major]中取出 file_operations 结构体,调用里面的open/read/write 函数指针。
上述说法并不准确,内核中确实有一个 chrdevs 数组:
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
去访问它的时候,并不是直接使用主设备号 major 来确定数组项,而是使用如下函数来确定数组项:
#define CHRDEV_MAJOR_HASH_SIZE 255
/* index in the above */
static inline int major_to_index(unsigned major)
{
return major % CHRDEV_MAJOR_HASH_SIZE;
}
上述代码中, CHRDEV_MAJOR_HASH_SIZE 等于 255。 比如主设备号 1、 256,都会使用 chardevs[1] 。 chardevs[1] 是一个链表 , 链表里有多 个char_device_struct 结构体, 某个结构体表示主设备号为 1 的设备, 某个结构体表示主设备号为 256 的设备。
chardevs 的结构图如图:
可以得出如下结论:
① chrdevs[i]数组项是一个链表头,链表里每一个元素都是一个 char_device_struct 结构体,每个元素表示一个驱动程序。char_device_struct 结构体内容如下:
static struct char_device_struct {
struct char_device_struct *next;
unsigned int major;
unsigned int baseminor;
int minorct;
char name[64];
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
它指定了主设备号 major、次设备号 baseminor、个数 minorct,在 cdev中含有 file_operations 结构体。char_device_struct 结 构 体 的 含 义 是 : 主 次 设 备 号 为 (major,baseminor)、 (major, baseminor+1)、 (major, baseminor+2)、 (major,baseminor+ minorct-1)的这些设备,都使用同一个 file_operations 来操作。
以前为了更容易理解驱动程序时,说“内核通过主设备号找到对应的file_operations 结构体”,这并不准确。应该改成:“内核通过主、次设备号,找到对应的 file_operations 结构体”。
② 在上图中, chardevs[1]中有 3 个驱动程序。第 1 个 char_device_struct 结构体对应主次设备号(1, 0)、 (1, 1),这是第 1 个驱动程序(minorct = 2)。
第 2 个 char_device_struct 结 构 体 对 应 主 次 设 备 号 (1, 2)、 (1,2)、……、 (1, 11),这是第 2 个驱动程序(minorct = 10)。
第 3 个 char_device_struct 结构体对应主次设备号(256, 0),这是第 3个驱动程序(minorct = 1)。
分配一个 cdev 结构体,并设置它:它含有 file_operations结构体
cdev = cdev_alloc();
if (!cdev)
goto out2;
// set数据
cdev->owner = fops->owner;
cdev->ops = fops;
kobject_set_name(&cdev->kobj, "%s", name);
dev_add 把 cdev 结构体注册进内核里
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
cdev_add 函数代码如下:
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
这个函数涉及 kobj 的操作,这是一个通用的链表操作函数。它的作用是:把 cdev 结构体放入 cdev_map 链表中 , 对应的索引值 是 “ dev ” 到“ dev+count-1”。以后可以从 cdev_map 链表中快速地使用索引值取出对应的cdev。
比如执行以下代码:
err = cdev_add(cdev, MKDEV(1, 2), 10);
其中的 MKDEV(1,2)构造出一个整数“ 1<<8 | 2”,即 0x102;上述代码将cdev 放入 cdev_map 链表中,对应的索引值是 0x102 到 0x10c(即 0x102+10)。以后根据这 10 个数值(0x102、 0x103、 0x104、……、 0x10c)中任意一个,都可以快速地从 cdev_map 链表中取出 cdev 结构体。
APP 打开某个字符设备节点时,进入内核。在内核里根据字符设备节点的主、次设备号,计算出一个数值(major<<8 | minor,即 inode->i_rdev),然 后 使 用 这 个 数 值 从 cdev_map 中 快 速 得 到 cdev , 再 从 cdev 中 得 到file_operations 结构体。关键函数如下
在打开文件的过程中,可以看到并未涉及 chrdevs,都是使用 cdev_map。所以可以看到在 chrdevs 的定义中看到如下注释
4.3 class_destroy/device_create 浅析
驱动程序的核心是 file_operations 结构体:分配、设置、注册它。“ class_destroy/device_create”函数知识起一些辅助作用:在/sys 目录下创建一些目录、文件,这样 Linux 系统中的 APP(比如 udev、 mdev)就可以根据这些目录或文件来创建设备节点。
以下代码将会在“ /sys/class”目录下创建一个子目录“ hello_class”:
hello_class = class_create(THIS_MODULE, "hello_class");
以下代码将会在 “ /sys/class/hello_class ”目录下创建一个文件“ hello”:
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
下图是我的板子情况
作者:StudyWinter