当前位置: 首页 > news >正文

ceph的块存储如何骗过服务器,让服务器把它当做真实的硬盘

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>#defineDEVICE_NAME"my_virtual_dev"// 设备名字#defineCLASS_NAME"my_class"// 设备类名字#defineBUFFER_SIZE1024// 虚拟内存缓冲区大小staticintmajor_num;// 主设备号staticstructclass*my_class=NULL;// 设备类staticcharkernel_buffer[BUFFER_SIZE];// 模拟设备的内核内存// 1. 实现设备的 open 操作staticintmy_open(structinode*inode,structfile*file){pr_info("虚拟设备被打开啦!\n");return0;}// 2. 实现设备的 read 操作(把内核数据拷贝给用户)staticssize_tmy_read(structfile*file,char__user*user_buf,size_tcount,loff_t*offset){intbytes_to_read;// 防止读取越界bytes_to_read=min(count,(size_t)(BUFFER_SIZE-*offset));if(bytes_to_read<=0)return0;// 安全地把数据从内核空间拷贝到用户空间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);returnbytes_to_read;}// 3. 实现设备的 write 操作(把用户数据拷贝给内核)staticssize_tmy_write(structfile*file,constchar__user*user_buf,size_tcount,loff_t*offset){intbytes_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);returnbytes_to_write;}// 4. 填充文件操作结构体(告诉内核我们实现了哪些功能)staticstructfile_operationsfops={.owner=THIS_MODULE,.open=my_open,.read=my_read,.write=my_write,};// 5. 驱动模块初始化函数(驱动被加载时执行)staticint__initsimple_char_init(void){// 动态申请一个主设备号major_num=register_chrdev(0,DEVICE_NAME,&fops);if(major_num<0){pr_err("申请主设备号失败!\n");returnmajor_num;}// 创建设备类,并在 /dev 下自动创建设备文件my_class=class_create(THIS_MODULE,CLASS_NAME);if(IS_ERR(my_class)){unregister_chrdev(major_num,DEVICE_NAME);pr_err("创建设备类失败!\n");returnPTR_ERR(my_class);}device_create(my_class,NULL,MKDEV(major_num,0),NULL,DEVICE_NAME);pr_info("驱动加载成功!主设备号是: %d\n",major_num);return0;}// 6. 驱动模块退出函数(驱动被卸载时执行)staticvoid__exitsimple_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. 加载驱动(需要管理员权限):

    sudoinsmod simple_char.ko

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

  3. 测试写入数据

    echo"Hello Linux Driver!"|sudotee/dev/my_virtual_dev

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

  4. 测试读取数据

    sudocat/dev/my_virtual_dev

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

  5. 查看内核打印的日志

    dmesg|tail

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

  6. 卸载驱动

    sudormmod 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>#defineINCLUDE_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);__visiblestructmodule__this_module__section(".gnu.linkonce.this_module")={.name=KBUILD_MODNAME,.init=init_module,#ifdefCONFIG_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 系统提供的全套专业编译服务!

http://www.jsqmd.com/news/862858/

相关文章:

  • 2026 渗透测试行业全景解析|机遇、挑战与未来趋势
  • 2026最新诚信优选 合肥市庐阳区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 广州市海珠区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • GP210:PWM 转 0/4-20mA,MCU 直接驱动工业电流输出
  • 本地 AI 编码助手从 0 配起来:先选模型,再接 Ollama、VS Code、Claude Code 和 Codex
  • 零基础跨行月入 10k|比起天赋,更重要的是破局思维
  • 2026最新诚信优选 合肥市蜀山区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 贵阳市花溪区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 广州市花都区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 一文搞懂达梦数据库全产品体系:DM8、DataWatch、DSC、DPC、DMHS、DEM、DTS
  • 手写一个mini版Spring:如何让容器能注册和获取单例 Bean
  • 2026最新诚信优选 合肥市瑶海区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 贵阳市南明区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026年期货期权程序化:主流工具品种覆盖与权限边界观察
  • 2026最新诚信优选 广州市黄埔区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 福州市鼓楼区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • Windows系统缺失ddraw.dll文件?游戏闪退、图形报错原因详解及处理办法
  • 2026最新诚信优选 河池市金城江区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026年Java八股文+场景题最全总结(真实大厂高频1000题)
  • 2026最新诚信优选 广州市荔湾区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 福州市晋安区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 贵阳市乌当区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 医护职称评审PPT制作指南:三种AI方法介绍
  • 2026最新诚信优选 广州市南沙区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 福州市马尾区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 河池市宜州区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 贵阳市云岩区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 桂林市叠彩区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 广州市天河区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026 最新!3 款亲测录音生成会议纪要神器,10 分钟出稿免费好用不踩坑