韦东山嵌入式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

    物联沃分享整理
    物联沃-IOTWORD物联网 » 韦东山嵌入式Linux系列实验一:入门初探

    发表回复