Linux 驱动开发入门

1. 编写一个简单的 Linux 内核模块

首先,我们需要理解什么是内核模块?简单来说,内核模块是一段 “固定格式” 的代码,像一个“插件”一样,linux 内核可以动态的加载并执行这段代码,也可以把这段代码编译进内核,在内核启动的时候来执行这段代码。

下面我们写一个简单的 linux 模块:

准备工作:

sudo apt install build-essential linux-headers-`uname -r`

创建我们的项目目录:

mkdir HelloModule
cd HelloModule

在 HelloModule 中添加 hello_module.c

#include <linux/module.h>
#include <linux/init.h>

/**
 * __init 是一个宏,表示 hello_init 是一个初始化函数,会放到编译后目标文件的初始化段中
 */ 
static int __init hello_init(void)
{
    //printk 是内核中的日志打印函数
    printk("Hello world!\n");
    return 0;
}

/**
 * __exit 是一个宏,表示 hello_exit 是一个初始化函数,会放到编译后目标文件的初始化段中
 */ 
static void __exit hello_exit(void)
{
    printk("hello exit\n");
}

/**
 * hello_init 是当前模块的启动函数
 */ 
module_init(hello_init);
/*
 * hello_exit 是当前模块的退出函数
 */
module_exit(hello_exit);

2. 将模块编译进内核

把 hello_module 移动到内核的 /drivers/char 目录:

cp hello_module.c  kernel目录/drivers/char

接下来我们修改 /drivers/char/Kconfig 文件,使得我们的 hello 模块,能出现在内核的编译选择中。

在 /drivers/char 中的 Kconfig 文件中添加:

config HELLO_MODULE
    bool "hello module support"
    default y

然后在 /drivers/char 下的 Makefile 文件中添加:

obj-$(CONFIG_HELLO_MODULE)       += hello_module.o

当在 make menuconfig 编译菜单中选中了 hello module support, CONFIG_HELLO_MODULE 的值是 y,没有选中值是 m(我们定义的默认值是 y):

  • obj-y += hello_module.o 的意思是将 hello_module.o 编译进内核
  • obj-m += hello_module.o 的意思是文件 hello_module.o 作为”模块”进行编译,不会编译到内核,但是会生成一个独立的 “test.ko” 文件

最后配置内核:

cp ./arch/x86/configs/x86_64_ranchu_defconfig .config
make menuconfig

进入 Device Drivers 选项:

进入 Character devices

这里就可以看见我们刚才添加的选项,默认是选上的。

然后执行编译:

#执行之前的编译脚本
sh build.sh

启动模拟器:

source build/envsetup.sh
lunch aosp_x86_64-eng
emulator -kernel ~/kernel/goldfish/arch/x86_64/boot/bzImage

查看开机信息:

# dmesg 用于显示开机信息
adb shell dmesg

3. linux 中文件的读写

驱动是干什么的?在驱动的相关书籍上,网络上你能看到很多专业的定义。我们暂时不关心这些专业的说法,仅从功能的角度来说,驱动程序使得应用程序可以访问硬件

那应用是如何访问硬件的?linux 中一切皆文件,访问硬件就是对文件的读写操作。比如 led 灯对应的文件是 /etc/led, 读写这个文件就能操作 led 灯。

接下来的问题就是,linux 中如何读写文件?

linux中文件读写相关的主要 api:

//打开文件
int open(const char *pathname, int flags, mode_t mode);
//从文件中读数据
ssize_t read(int fd, void *buf, size_t count);
//向文件中写数据
ssize_t write(int fd, const void *buf, size_t count);
//专用于设备输入输出操作
int ioctl(int fd, unsigned long request, ...);
//关闭文件的读写,回收资源
int close(int fd);

函数的具体用法不是本文的重点,有兴趣的同学可以学习 Linux程序设计 的第二章。工作中忘了,可以通过 man 命名查看具体用法。

下面来看一下 open 函数:

//该函数用于打开文件
int open(const char *pathname, int flags, mode_t mode);

当打开一个文件的时候,会返回一个 int 值,一般称这个返回值为句柄或者 handle,在内核中,句柄是一个数组的索引(index),数组的成员是 struct file :

struct file {
        union {
                struct llist_node       fu_llist;
                struct rcu_head         fu_rcuhead;
        } f_u;
        struct path             f_path;
        struct inode            *f_inode;       /* cached value */
        const struct file_operations    *f_op;   //关注1

        /*
         * Protects f_ep_links, f_flags.
         * Must not be taken from IRQ context.
         */
        spinlock_t              f_lock;
        enum rw_hint            f_write_hint;
        atomic_long_t           f_count;
        unsigned int            f_flags;  //关注2
        fmode_t                 f_mode;      //关注3
        struct mutex            f_pos_lock;
        loff_t                  f_pos;      //关注4
        struct fown_struct      f_owner;
        const struct cred       *f_cred;
        struct file_ra_state    f_ra;

        u64                     f_version;
#ifdef CONFIG_SECURITY
        void                    *f_security;
#endif
        /* needed for tty driver, and maybe others */
        void                    *private_data;

#ifdef CONFIG_EPOLL
        /* Used by fs/eventpoll.c to link all the hooks to this file */
        struct list_head        f_ep_links;
        struct list_head        f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
        struct address_space    *f_mapping;
        errseq_t                f_wb_err;
} __randomize_layout
  __attribute__((aligned(4)));

struct file 的结构有点复杂,入门阶段主要关注代码中标注的四个关注点。

在内核中,有一个 struct file 的数组,当调用 open 函数打开一个文件的时候,内核就会构建一个 struct file,并添加到这个数组中,返回 struct file 在数组中的 index 给用户态程序,这个值就是 open 函数的返回值。

根据文件的命名,容易猜出:使用 open 打开文件时,传入的 flags、mode 等参数会被记录在内核中,具体如下图所示:

struct file 有一个成员为 file_operations:

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 *);  //关注点1
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); //关注点2
    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    int (*iterate) (struct file *, struct dir_context *);
    int (*iterate_shared) (struct file *, struct dir_context *);
    unsigned int (*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);//关注点3
    int (*mmap) (struct file *, struct vm_area_struct *);//关注点4
    int (*open) (struct inode *, struct file *);//关注点5
    int (*flush) (struct file *, fl_owner_t id);//关注点6
    int (*release) (struct inode *, struct file *);//关注点7
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    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);
    int (*setlease)(struct file *, long, 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);
    int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
            u64);
    ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
            u64);
} __randomize_layout;

内部主要是一些函数指针,我们主要关注常用的几个函数:

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);

这些函数都是由相应硬件驱动程序提供。

至此,文件读写的大致流程就出来了:

  • app 调用 open read 等系统调用函数
  • 内核构建相应的 struct file,并添加进数组,返回 index 给 app
  • 调用驱动程序 file_operations 指针提供的 open read 等函数,完成实际的硬件操作

4. hello 驱动的编写

驱动就是一个模块,在模块的基础上添加驱动框架和硬件操作的部分就可以完成驱动程序的编写了。下面我们写一个 hello 驱动,这个驱动只是简单的在用户态和内核态之间拷贝数据,没有实际的硬件操作,仅用于流程的展示。编写驱动的步骤如下:

  1. 确定主设备号,也可以让内核分配 (设备号就是硬件的一个编号)
  2. 定义自己的 file_operations 结构体
  3. 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
  4. 定义 init 函数,在 init 函数中调用 register_chrdev 注册函数
  5. 定义 exit 函数,在 exit 函数中调用 unregister_chrdev 卸载函数
  6. 其他完善:提供设备信息,自动创建设备节点:class_create, device_create

在 ~/Project 目录下创建如下的目录结构:

HelloDriver
├── build_driver.sh
├── build_driver_test.sh
├── CMakeLists.txt
├── hello_drv.c
├── hello_drv_test.c
└── Makefile

其中 hello_drv.c:

#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. 实现对应的open/read/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__);
    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__);
    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结构体告诉内核:注册驱动程序                                */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init hello_init(void)
{
    int err;

    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    major = register_chrdev(0, "hello", &hello_drv);  /* /dev/hello */

    //提供设备信息,自动创建设备节点。
    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_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
    //到这里我们就可以通过 /dev/hello 文件来访问我们的驱动程序了。
    return 0;
}

/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
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. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");

可以看出,除了定义 read write 等函数,其他都是一些模板代码。

接着我们再写一个测试程序:

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 来编译驱动模块,然后通过命令行加载和卸载驱动程序。这里介绍第二种方法:

创建 Makefile 文件:

KERN_DIR = /home/zzh0838/kernel/goldfish

all:
    make -C $(KERN_DIR) M=`pwd` modules 

clean:
    make -C $(KERN_DIR) M=`pwd` modules clean
    rm -rf modules.order
    rm -f hello_drv_test

obj-m    += hello_drv.o

-C 选项的作用是指将当前工作目录转移到你所指定的位置。“M=”选项的作用是,当用户需要以某个内核为基础编译一个外部模块的话,需要在 make modules 命令中加入“M=dir”,程序会自动到你所指定的 dir 目录中查找模块源码,将其编译,生成 KO 文件。

编写编译驱动的脚本 build_driver.sh:

#!/bin/bash
export ARCH=x86_64
export SUBARCH=x86_64
export CROSS_COMPILE=x86_64-linux-android-
export PATH=~/aosp/prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.9/bin:$PATH
make

执行 ./build_driver.sh,编译出 hello_drv.ko,接下来启动模拟器,把 ko 文件上传到模拟器:

cd aosp目录
source build/envsetup.sh
lunch aosp_x86_64-eng
emulator -kernel ~/kernel/goldfish/arch/x86_64/boot/bzImage

cd hellodriver
# 使用 adb 上传 ko 文件
adb push hello_drv.ko /data/local/tmp
# 进入模拟器的 shell 环境
adb shell 
cd /data/local/tmp
#加载模块,加载完成后,/dev 目录下就会有一个 hello 文件
insmod hello_drv.ko
ls /dev/hello -l

通过 cmake 的方式来编译测试程序:
编写 CMakeLists.txt:

cmake_minimum_required(VERSION 3.0)

project(test)

add_executable(${PROJECT_NAME} hello_drv_test.c)

编写编译脚本 build_driver_test.sh:

export ANDROID_NDK=你的ndk安装目录

rm -r build
mkdir build && cd build 

# CMake的内置支持
# cmake -DCMAKE_SYSTEM_NAME=Android \
#     -DCMAKE_SYSTEM_VERSION=29 \
#     -DCMAKE_ANDROID_ARCH_ABI=x86_64 \
#     -DANDROID_NDK=$ANDROID_NDK \
#     -DCMAKE_ANDROID_STL_TYPE=c++_shared \
#     ..

# 工具链文件支持
cmake \
    -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=x86_64 \
    -DANDROID_PLATFORM=android-29 \
    -DANDROID_STL=c++_shared \
      ..

cmake --build .

编译程序并上传模拟器:

# 编译
sh ./build_driver_test.sh
# 打开模拟器,流程略
# 上传可执行文件
adb push build/test /data/local/tmp
# 进入到模拟器 shell
adb shell
# 执行程序
cd /data/local/tmp
# 加载驱动程序
insmod hello_drv.ko
./test -w "nihao"
./test -r

执行程序的结果如下所示:

参考资料

下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/20555,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?