ceph的块存储,就是一块远程网络硬盘。操作系统为啥会读写这块假硬盘呢?

一台服务器要使用CEPH提供的块存储,也是需要ceph的驱动软件来和ceph通讯吧

是的,你的理解完全正确。一台服务器想要使用 Ceph 提供的块存储,必须通过相应的“驱动软件”或客户端组件来和 Ceph 集群进行通讯。

在 Ceph 的块存储(RBD, RADOS Block Device)体系中,这种“驱动软件”主要分为两种实现路径:

1. 内核态驱动(KRBD)
这是最常见、最接近传统硬盘使用习惯的方式。

  • 原理:它依赖 Linux 内核自带的 rbd 模块(rbd.ko)。
  • 使用方式:你需要在服务器上安装 Ceph 的客户端软件(如 ceph-common 包),然后使用 rbd map 命令将 Ceph 集群里的一个块设备镜像(Image)映射到本地。
  • 效果:映射成功后,你的服务器本地会出现一个标准的块设备文件(例如 /dev/rbd0)。你可以像对待一块普通的物理硬盘一样,对它进行分区、格式化(如 mkfs.ext4)并挂载到某个目录下使用。

2. 用户态库(librbd)
这种方式不通过内核,而是直接在应用程序层面调用 Ceph 提供的动态链接库。

  • 原理:应用程序直接链接并调用 Ceph 提供的 librbdlibrados 库文件来读写数据。
  • 使用方式:这种方式不会在操作系统里生成 /dev/rbd0 这样的设备文件,而是由应用程序直接对接。
  • 典型场景:主要被 KVM/QEMU 虚拟机或 OpenStack 等云平台使用。比如,当你启动一台使用 Ceph 作为后端的虚拟机时,宿主机上的 QEMU 进程就是通过 librbd 直接与 Ceph 集群通讯,为虚拟机提供磁盘空间的。

总结来说:
如果你是想把 Ceph 块存储当成一块普通的远程硬盘挂载到 Linux 服务器上,你需要安装 ceph-common 客户端,并依赖内核的 KRBD 驱动;如果你是开发应用程序或者配置 KVM 虚拟机来直接使用 Ceph 存储,则是通过 librbd 库来实现通讯的。

自己实现一个最简的远程磁盘及协议,要怎么做

自己实现一个最简的远程磁盘系统,核心原理其实就是**“网络IO”与“本地磁盘IO”的桥接**。简单来说,就是把客户端(Client)对磁盘的读写请求,通过网络发给服务端(Server),服务端在本地执行真实的读写操作后,再把结果返回给客户端。

我们可以把这个系统拆解为以下几个核心步骤来实现:

1. 通信层:建立网络通道

远程磁盘通常追求较高的读写性能,因此底层通信协议一般首选 TCP 而不是 UDP(TCP 能保证数据可靠传输,避免磁盘数据丢包损坏)。

  • 服务端:启动一个 TCP 监听端口(比如 8888),等待客户端连接。
  • 客户端:通过 IP 和端口连接到服务端,建立一个稳定的长连接。

2. 协议层:设计“读写指令”

为了让服务端听懂客户端要干什么,我们需要自定义一个简单的通信协议(报文格式)。一个最简的远程磁盘协议,只需要包含以下几种核心指令(可以用一个字节来表示操作类型 opcode):

  • READ(读,比如 opcode = 0x01):客户端告诉服务端“我要读数据”。
  • WRITE(写,比如 opcode = 0x02):客户端告诉服务端“我要写数据”。
  • ACK/RESPONSE(应答):服务端处理完后,返回成功状态或读取到的数据。

请求报文的结构可以设计得非常简单,例如:
[操作码(1字节)] + [偏移量 offset(8字节)] + [数据长度 length(4字节)] + [实际数据(写操作时附带)]
(注:在网络传输多字节整数时,记得使用大端序(htons/htonl)来保证不同架构机器间的兼容性。)

3. 服务端实现:本地磁盘的“代笔者”

服务端的逻辑非常直观,它本质上就是一个拿着“后端真实磁盘文件”的打工人:

  1. 在启动时,服务端在本地创建一个巨大的空白文件(比如 disk.img,用 dd 命令生成),这个文件就代表一块“虚拟硬盘”。
  2. 接收到客户端的 READ 请求后,解析出 offset(偏移量)和 length(长度),使用系统调用(如 seekread)去读取 disk.img 对应位置的数据,然后通过网络发回给客户端。
  3. 接收到 WRITE 请求后,解析出 offsetlength 和附带的“实际数据”,使用 seekwrite 把数据精准地写入 disk.img 的对应位置。

4. 客户端实现:虚拟块设备驱动

这是最难、也是最核心的一步。客户端不能只是一个普通的发请求的程序,它必须让操作系统认为**“我插上了一块新硬盘”**。

  • Linux 实现思路:你需要编写一个 Linux 内核模块,利用 Linux 内核提供的 ioctl 接口。当客户端程序通过网络收到服务端发来的数据时,通过 ioctl 把这些数据“喂”给内核的块设备层。这样,Linux 系统里就会多出一个 /dev/remote_disk0 的设备文件。
  • 使用效果:一旦这个设备文件出现,你就可以像操作普通硬盘一样,对它执行 mkfs.ext4 格式化,然后 mount 挂载到某个目录,随意读写文件。

5. 进阶完善(工程化细节)

当你把上面最简的“读写通路”跑通后,一个真正可用的远程磁盘还需要考虑以下问题:

  • 分包与粘包处理:网络传输有 MTU(最大传输单元)限制,大数据块需要拆分成小包发送,并在协议头加上数据包的总长度,防止接收方读取时发生粘包。
  • 断点续传与重试机制:网络可能会抖动,如果写入一半断网了,需要有 ACK 确认机制和超时重传逻辑,保证数据的一致性。
  • 权限与加密:真实的远程磁盘不能谁都能连,需要加上简单的身份验证,甚至给 TCP 通道套上 TLS/SSL 加密,防止数据在局域网内被窃听。

总结一下:
实现一个最简远程磁盘,本质上就是**“客户端内核驱动拦截IO请求 -> 封装成自定义协议 -> 走TCP网络发给服务端 -> 服务端落地到本地大文件”**的完整闭环。这不仅是网络编程的硬核练手项目,也能让你深刻理解 Ceph RBD、iSCSI 等成熟块存储产品的底层运作原理。


linux驱动开发的原理和思路是什么,请介绍的通俗易懂一些

Linux 驱动开发的本质,其实就是给操作系统写一个硬件的“专属翻译官”

因为硬件(比如显卡、网卡、传感器)只听得懂底层的电信号和寄存器指令,而 Linux 内核和应用软件只懂标准的系统指令。驱动就是夹在中间的那层软件,负责把上层的指令“翻译”成硬件能懂的操作,再把硬件的反馈“翻译”回上层。

为了让你更直观地理解,我们可以把 Linux 驱动开发的原理和思路拆解为以下几个核心维度:

🧩 核心思路:一切皆文件

这是 Linux 驱动开发最底层的哲学。在 Linux 看来,不管是普通的文本文件,还是键盘、鼠标、硬盘等硬件设备,统统都被抽象成了一个“文件”

  • 对用户来说:操作硬件就像操作文件一样简单。比如你想点亮一个 LED 灯,不需要去研究电路,只需要向一个特定的设备文件(比如 /dev/led)执行“写入”操作;想读取温湿度传感器的数据,只需要从这个设备文件里“读取”数据。
  • 对驱动来说:你的工作就是写好这个“文件”背后的逻辑。你需要告诉系统,当用户对这个文件执行 open(打开)、read(读)、write(写)时,驱动底层具体要去操作硬件的哪些寄存器、拉高还是拉低哪个引脚的电平。

🏗️ 核心模型:总线-设备-驱动(Bus-Device-Driver)

在早期的驱动开发中,硬件信息和驱动代码经常混杂在一起,导致代码极其难维护。现代 Linux 驱动开发采用了**“总线-设备-驱动”模型**,核心思想就是**“分离与解耦”**。

我们可以用**“开餐厅”**来打个比方:

  • 设备(Device):就是**“食材”**(比如鱼、肉、蔬菜),它只负责描述硬件的客观属性(比如硬件的内存地址、中断号等),不包含任何处理逻辑。
  • 驱动(Driver):就是**“厨师”**(比如王师傅只做鱼,李师傅只做肉),它只包含处理逻辑(怎么切、怎么炒),但不关心食材具体放在哪个冰箱里。
  • 总线(Bus):就是餐厅的**“调度员”**。它负责把“食材”和“厨师”匹配起来。

开发思路变成了:你只需要在代码里告诉内核“我是哪个总线的驱动,我能处理哪种设备”,内核的总线调度员就会自动帮你完成匹配。一旦匹配成功,就会自动调用驱动里的初始化函数(在代码中通常叫 probe 函数),开始干活。

🛠️ 开发流程“三步曲”

无论开发哪种驱动,核心流程都可以简化为以下三个步骤:

  1. “报名”——注册驱动
    在驱动模块加载时,通过特定的注册函数告诉内核:“嗨!我来了,我能管理某某硬件!”
  2. “干活”——实现核心功能
    这是驱动的灵魂。你需要填充一个名为 file_operations 的结构体(可以理解为一张任务清单),把你的 openreadwrite 等函数填进去。当用户程序调用系统指令时,内核就会拿着这张清单,精准地执行你写好的硬件操作代码。
  3. “卸任”——卸载驱动
    在驱动模块退出时,负责释放占用的内存、注销设备,把资源干干净净地交还给系统。

📚 驱动家族的“三大门派”

Linux 根据硬件传输数据的方式不同,把驱动分成了三大类:

驱动类型 核心特点 典型代表
字符设备驱动 像水流一样,按“字节”顺序传输,不能随机乱跳 键盘、鼠标、串口、各类传感器
块设备驱动 像集装箱运输,按“块”(如4KB)批量传输,支持随机读写 硬盘、U盘、SSD(也就是你刚才问的磁盘)
网络设备驱动 专门处理网络数据包,不走文件系统的 read/write 逻辑 以太网卡、Wi-Fi 模块

总结一下:
Linux 驱动开发就是利用**“一切皆文件”的哲学,通过“总线-设备-驱动”**的解耦模型,去编写一套标准化的代码(填好 file_operations 这张表),让操作系统能够统一、高效地指挥各种各样的硬件为你工作。


驱动注册后,设备从哪里来的?

驱动注册后,“设备”的来源其实分为两个层面:一个是物理上真实的硬件,另一个是软件上供你调用的设备文件(如 /dev/xxx

我们可以结合你刚才了解的“总线-设备-驱动”模型,来看看设备到底是从哪里“冒”出来的:

1. 物理设备从哪来?(硬件的“自我介绍”)

物理设备并不是驱动“变”出来的,而是原本就存在的。驱动注册后,内核会帮你在系统里“找到”这些设备。主要有两种情况:

  • 热插拔发现(即插即用):当你把 U 盘、鼠标等 USB 设备插入电脑时,硬件控制器会立刻感知到电压变化,并向内核发送一个中断信号。内核收到信号后,会马上读取这个新设备的描述符(比如厂商 ID、产品 ID),然后在系统内部登记:“嘿,总线上来了个新家伙!”。
  • 系统启动枚举(开机自检):对于插在主板上的 PCIe、SATA 等固定设备,系统在上电启动时,内核的总线代码会主动去扫描物理总线,把系统里所有的硬件设备都“点名”一遍,登记在自己的花名册上。

驱动注册后的动作:当你调用 xxx_driver_register() 注册驱动时,其实只是向内核提交了一份“简历”(包含你能支持的设备 ID 等信息)。内核会拿着你的简历,去和它花名册上已经登记的设备进行匹配。一旦匹配成功,内核就会自动调用你驱动里的 probe(探测)函数,相当于把硬件正式交到你手里去接管。

2. 设备文件从哪来?(软件的“自动建档”)

物理设备被驱动接管后,为了让用户空间(也就是你的应用程序)能方便地操作它,系统还需要在 /dev/ 目录下生成一个对应的设备文件(比如 /dev/sda/dev/my_led)。这个过程在现代 Linux 中是全自动的:

  • 驱动“上报户口”:在你的驱动代码(通常在 probe 函数里)接管硬件后,会调用 class_create()device_create() 这两个内核 API。这一步相当于驱动向系统大声汇报:“我已经成功接管了这个硬件,它的设备号是 XXX,请给它建个档案!”。
  • 内核“发广播”:内核收到汇报后,会在内部的 sysfs 文件系统(/sys/class/ 目录)里记录下这个设备的详细信息,同时向用户空间发送一个名为 uevent(用户事件)的广播通知。
  • 管家“自动建档”:在用户空间,有一个 7x24 小时值班的“管家”守护进程(通常是 udev,在嵌入式系统中是它的精简版 mdev)。它时刻监听内核发出的 uevent 广播。一旦收到“有新设备加入”的通知,它就会自动在 /dev/ 目录下创建出对应的设备节点文件。

简单总结一下:
驱动注册后,物理设备是内核通过“扫描”或“监听中断”早就发现并登记好的;而设备文件则是驱动在成功接管硬件后,通过 device_create() 上报,最终由系统里的 udev/mdev 管家自动在 /dev/ 目录下创建出来的。

请写一个最简单的驱动程序,我学习一下

结合你之前对“虚拟设备”和“驱动原理”的理解,写一个最简单的 Linux 驱动,最好的方式就是写一个**“虚拟内存字符设备”**。

它的功能非常直观:你在内核里开辟一块内存当作“虚拟设备”,用户往里面写数据,它就存起来;用户读数据,它就把存的内容返回给你。

下面我们用大概 100 行代码,带你一步步实现这个驱动。

第一步:编写驱动源码

创建一个名为 simple_char.c 的文件,把下面的代码复制进去:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/device.h>

#define DEVICE_NAME "my_virtual_dev"  // 设备名字
#define CLASS_NAME  "my_class"        // 设备类名字
#define BUFFER_SIZE 1024              // 虚拟内存缓冲区大小

static int major_num;                 // 主设备号
static struct class* my_class = NULL; // 设备类
static char kernel_buffer[BUFFER_SIZE]; // 模拟设备的内核内存

// 1. 实现设备的 open 操作
static int my_open(struct inode *inode, struct file *file) {
    pr_info("虚拟设备被打开啦!\n");
    return 0;
}

// 2. 实现设备的 read 操作(把内核数据拷贝给用户)
static ssize_t my_read(struct file *file, char __user *user_buf, size_t count, loff_t *offset) {
    int bytes_to_read;
    // 防止读取越界
    bytes_to_read = min(count, (size_t)(BUFFER_SIZE - *offset));
    if (bytes_to_read <= 0) return 0;
    
    // 安全地把数据从内核空间拷贝到用户空间
    if (copy_to_user(user_buf, kernel_buffer + *offset, bytes_to_read) != 0) {
        return -EFAULT;
    }
    *offset += bytes_to_read;
    pr_info("给用户返回了 %d 个字节的数据\n", bytes_to_read);
    return bytes_to_read;
}

// 3. 实现设备的 write 操作(把用户数据拷贝给内核)
static ssize_t my_write(struct file *file, const char __user *user_buf, size_t count, loff_t *offset) {
    int bytes_to_write;
    bytes_to_write = min(count, (size_t)(BUFFER_SIZE - *offset));
    if (bytes_to_write <= 0) return -ENOSPC;
    
    // 安全地把数据从用户空间拷贝到内核空间
    if (copy_from_user(kernel_buffer + *offset, user_buf, bytes_to_write) != 0) {
        return -EFAULT;
    }
    *offset += bytes_to_write;
    pr_info("从用户那里接收了 %d 个字节的数据\n", bytes_to_write);
    return bytes_to_write;
}

// 4. 填充文件操作结构体(告诉内核我们实现了哪些功能)
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .read = my_read,
    .write = my_write,
};

// 5. 驱动模块初始化函数(驱动被加载时执行)
static int __init simple_char_init(void) {
    // 动态申请一个主设备号
    major_num = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_num < 0) {
        pr_err("申请主设备号失败!\n");
        return major_num;
    }

    // 创建设备类,并在 /dev 下自动创建设备文件
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        unregister_chrdev(major_num, DEVICE_NAME);
        pr_err("创建设备类失败!\n");
        return PTR_ERR(my_class);
    }
    device_create(my_class, NULL, MKDEV(major_num, 0), NULL, DEVICE_NAME);

    pr_info("驱动加载成功!主设备号是: %d\n", major_num);
    return 0;
}

// 6. 驱动模块退出函数(驱动被卸载时执行)
static void __exit simple_char_exit(void) {
    device_destroy(my_class, MKDEV(major_num, 0)); // 销毁设备文件
    class_destroy(my_class);                       // 销毁设备类
    unregister_chrdev(major_num, DEVICE_NAME);     // 释放主设备号
    pr_info("驱动已安全卸载!\n");
}

module_init(simple_char_init);
module_exit(simple_char_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("YourName");
MODULE_DESCRIPTION("A Simple Virtual Character Driver");

第二步:编写 Makefile

在同一个目录下创建一个名为 Makefile 的文件(注意首字母大写,没有后缀):

obj-m += simple_char.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

第三步:编译并运行测试

你需要一台 Linux 电脑(虚拟机或物理机均可),打开终端,进入这两个文件所在的目录,依次执行以下命令:

  1. 编译驱动

    make
    

    编译成功后,当前目录下会生成一个 simple_char.ko 文件,这就是你的驱动模块。

  2. 加载驱动(需要管理员权限):

    sudo insmod simple_char.ko
    

    此时,驱动已经运行在内核里了。你可以用 ls /dev/my_virtual_dev 看看,系统已经自动帮你生成了这个设备文件。

  3. 测试写入数据

    echo "Hello Linux Driver!" | sudo tee /dev/my_virtual_dev
    

    这行命令会把字符串写入你的虚拟设备,驱动里的 my_write 函数会被触发。

  4. 测试读取数据

    sudo cat /dev/my_virtual_dev
    

    你会看到终端打印出 Hello Linux Driver!,这说明驱动里的 my_read 函数把刚才存在内核内存里的数据又还给了你。

  5. 查看内核打印的日志

    dmesg | tail
    

    你会看到驱动里用 pr_info 打印的“虚拟设备被打开啦”、“接收了xx字节”等调试信息。

  6. 卸载驱动

    sudo rmmod simple_char
    

    卸载后,/dev/my_virtual_dev 文件也会自动消失。

通过这个最简的驱动,你就完整走通了**“编写驱动代码 -> 编译成模块 -> 加载到内核 -> 自动创建设备文件 -> 用户空间读写测试 -> 卸载驱动”**的整个 Linux 驱动开发闭环。你可以试着修改一下代码,比如在 write 里加一些特殊的逻辑,来加深对它的理解!


试了一下,成功实现了一个最简驱动

$ insmod simple_char.ko
(gaofeng1120) $ cat /dev/my_virtual_dev
(gaofeng1120) $ echo "Hello Linux Driver2!" > /dev/my_virtual_dev
(gaofeng1120) $ cat /dev/my_virtual_dev
Hello Linux Driver2!

simple_char.mod.c 是哪里来的,为什会有这个文件

simple_char.mod.c 这个文件并不是你手写的,而是Linux 内核构建系统(Kbuild)在编译过程中自动生成的临时文件

你可以把它理解为驱动模块的**“专属身份证”“元数据说明文件”**。

🛠️ 它是怎么来的?

当你执行 make 命令编译驱动时,内核的构建系统会按照以下流水线工作:

  1. 首先,把你手写的 simple_char.c 编译成目标文件 simple_char.o
  2. 紧接着,构建系统会自动生成 simple_char.mod.c,并把它编译成 simple_char.mod.o
  3. 最后,链接器会把 simple_char.osimple_char.mod.o 打包链接在一起,生成最终的内核模块文件 simple_char.ko

📄 为什么需要这个文件?

Linux 内核模块(.ko)和普通的应用程序不同,它需要携带一些额外的“身份信息”才能被内核正确加载和识别。simple_char.mod.c 的作用就是存放这些内核专属的元数据,主要包括:

  1. 模块的许可证与描述信息:把你代码里写的 MODULE_LICENSE("GPL")MODULE_AUTHORMODULE_DESCRIPTION 等宏定义,转换成内核能够识别的标准数据结构。
  2. 模块的版本依赖(Magic 校验):记录当前模块是基于哪个版本的内核编译的。当你用 insmod 加载驱动时,内核会检查这些信息,防止你把一个为旧内核编译的驱动强行加载到新内核里,从而避免系统崩溃(即所谓的“版本魔术不匹配”)。
  3. 符号依赖信息:如果你的驱动用到了其他内核模块导出的函数,这些信息也会被记录在这里,方便内核在加载时自动处理依赖关系。

🔍 里面到底写了什么?

如果你好奇,完全可以打开 simple_char.mod.c 看一眼。你会发现它里面的代码非常枯燥,基本全是自动生成的宏和结构体数组,比如长这样(仅作示意):

#include <linux/module.h>
#define INCLUDE_VERMAGIC
#include <linux/build-salt.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>

BUILD_SALT;

MODULE_INFO(vermagic, VERMAGIC_STRING);
MODULE_INFO(name, KBUILD_MODNAME);

__visible struct module __this_module
__section(".gnu.linkonce.this_module") = {
    .name = KBUILD_MODNAME,
    .init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
    .exit = cleanup_module,
#endif
    .arch = MODULE_ARCH_INIT,
};

总结一下: simple_char.mod.c 是编译内核模块时由系统自动生成的“中间产物”,它负责把模块的身份信息和版本校验等元数据打包进最终的 .ko 文件里。你平时开发完全不用管它,执行 make clean 时它也会被自动清理掉。

没有看到makefile中调用kbuild啊, make -C /lib/modules/(shelluname−r)/buildM=(shell uname -r)/build M=(shellunamer)/buildM=(PWD) modules 这句话详细解释一下

你观察得很仔细!确实,在这个 Makefile 里我们并没有直接写 kmake 这个词,但你写的这句 make -C ... 实际上就是在调用 Linux 内核的构建系统(也就是 Kbuild)

在 Linux 内核开发中,kbuild 并不是一个独立的可执行程序的名字,而是一整套构建规则和脚本的统称。这套规则的核心,其实就是内核源码顶层目录下的那个 Makefile

下面为你逐字逐句拆解这句核心命令:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

1. make

这就是 Linux 系统自带的标准 GNU Make 工具。

2. -C /lib/modules/$(shell uname -r)/build

  • -C:这是标准 make 命令的一个选项,意思是“Change Directory”(切换目录)。它会让 make 在执行任何操作之前,先跳转到后面指定的那个目录里去
  • /lib/modules/$(shell uname -r)/build:这是你当前运行的 Linux 系统的内核源码(或内核头文件)目录。
    • $(shell uname -r):这是一个 Makefile 的语法,它会执行系统命令 uname -r 来获取你当前的内核版本号(比如 5.15.0-76-generic)。
    • 这个路径下通常有一个软链接,指向了真正的内核源码或头文件目录。
  • 合起来的意思是:让 make 工具暂时离开你当前的驱动目录,带着内核顶层 Makefile(也就是 Kbuild 的核心规则)去干活

3. M=$(PWD)

  • M=:这是专门传递给**内核顶层 Makefile(Kbuild)**的一个特殊变量。它的作用是告诉内核的构建系统:“虽然我切换到了内核目录,但我真正要编译的模块源码,其实是在 M 指定的这个目录里”。
  • $(PWD):这也是 Makefile 的语法,代表“Print Working Directory”(当前工作目录),也就是你存放 simple_char.c 和你的 Makefile 的这个文件夹。
  • 合起来的意思是:告诉内核 Kbuild 系统,编译完必要的依赖后,请回到我这个驱动源码目录里,把我的代码编译了

4. modules

  • 这是内核顶层 Makefile 里预定义好的一个编译目标(Target)
  • 当你跳转到内核目录后,执行 make modules,就会触发 Kbuild 里编译外部模块的整套复杂规则(包括生成你刚才问的 .mod.c 文件、进行版本校验、最后链接成 .ko 等)。

💡 总结一下整个执行流程(也就是 Kbuild 的“回调”机制):

  1. 你在驱动目录输入 make,你的 Makefile 开始执行。
  2. 遇到 make -C /内核目录 M=$(PWD) modules
  3. make 带着指令跳转到了 /内核目录,找到了内核的顶层 Makefile这时候 Kbuild 系统就被激活了)。
  4. 内核顶层 Makefile 看到 M=$(PWD),知道是要编译外部模块,于是设置好各种复杂的环境变量和编译规则。
  5. 内核顶层 Makefile 执行到 modules 目标时,会**再次回调(跳回)**到你的驱动目录(M 指定的目录),去读取你的 obj-m += simple_char.o,并开始编译你的 simple_char.c
  6. 编译完你的 .o 文件后,Kbuild 系统会自动生成 simple_char.mod.c,并最终把所有东西打包链接成 simple_char.ko

所以,虽然你没看到 kmake 这个命令,但通过 make -C ... M=... modules 这种精妙的“跳转+回调”机制,你的驱动已经完整地享受了 Linux 内核 Kbuild 系统提供的全套专业编译服务!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐