【Linux驱动开发】第一天:用户态与内核态通俗讲解+最简字符设备驱动实战

一、通俗类比:把Linux系统比作国际机场

快速建立认知,秒懂底层权限模型:

计算机系统 国际机场 类比
硬件资源(CPU、内存、硬盘、外设) 机场跑道、设施、物资、场地
Linux 内核 机场管理局+空管+工作人员
用户态应用(APP、命令行、业务程序) 普通乘客
系统调用 syscall 安检口/服务窗口(唯一进出通道)
CPU特权级 Ring0/Ring3 权限门禁系统
内核Panic崩溃 全场机场停运瘫痪

简单一句话:
用户态是受限游客,内核态是最高权限管理员。


二、用户态 & 内核态 四大核心区别

1. 权限等级完全不同

  • 用户态(Ring3)

    权限严格受限,只能执行普通指令;

    无法直接操作硬件、读写物理内存、访问寄存器、修改系统配置;

    所有硬件/资源操作,必须通过系统调用向内核申请。

  • 内核态(Ring0)

    CPU最高特权级,掌控系统所有资源;

    可直接读写硬件寄存器、操作物理内存、控制中断、调度进程;

    拥有绝对控制权。

2. 内存地址空间完全隔离

  • 用户态:每个进程拥有独立虚拟地址空间 ,进程之间互相隔离;
    单个应用崩溃、卡死,不会影响系统和其他程序。
  • 内核态:所有驱动、内核代码共享同一个全局地址空间
    驱动野指针、内存越界、非法操作,直接触发内核崩溃、整机重启。

3. 工作模式不同

  • 用户态:被动服务,只能「请求内核干活」;
  • 内核态:直接底层操作,绕过所有限制,直接操控硬件。

4. 上下文环境差异(驱动重点)

  • 用户态:只有进程上下文,允许睡眠、阻塞、等待IO;
  • 内核态:存在进程上下文 + 中断上下文
    中断上下文禁止睡眠、禁止阻塞,对代码执行要求极高。

三、关键问题:驱动为什么不能直接用C库函数?

作为Linux应用开发者,日常习惯使用 printf、malloc、fopen、free 等C库函数,但内核驱动完全不能使用,核心原因:

1. 内核不链接 glibc 标准库

C库函数是用户态专属运行库,驱动编译只会链接内核原生头文件,printf、malloc 会直接报未定义引用,编译失败。

2. C库函数依赖用户态运行环境

  • 依赖 stdout、文件描述符、用户态堆管理器
  • 内核无标准输出、无用户堆、无进程资源管理体系,环境完全不匹配。

3. 大量C库函数会睡眠阻塞

malloc、fread、printf 等函数可能因内存不足、IO阻塞进入睡眠;
中断上下文绝对禁止睡眠,一旦调用直接系统死机。

4. 用户态与内核态内存严格隔离

不能直接使用 strcpy、memcpy 跨内存空间拷贝;

必须使用内核专用函数 copy_to_user、copy_from_user 安全交互。

通俗总结

C库是给「普通乘客」用的服务,

驱动属于「内核管理员」,有自己的一套原生工具API,完全不通用。


四、极简字符设备驱动 完整源码

环境:Ubuntu 20.04 + Linux 5.15

1. 驱动源码 chr_dev.c

c 复制代码
// 内核专属头文件,替代用户态 stdio.h / stdlib.h
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/uaccess.h>

// 开源协议必须声明,否则内核告警
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux驱动学习");
MODULE_DESCRIPTION("入门级字符设备驱动");

// 自定义设备名称
#define DEV_NAME    "mychrdev"
#define CLASS_NAME  "mychr_class"

// 内核全局变量
static dev_t dev_num;
static struct class *dev_class;
static struct device *dev_device;

/**
 * @brief 驱动read回调,对应用户态read系统调用
 * @file 文件结构体
 * @buf 用户态缓冲区指针
*/
static ssize_t dev_read(struct file *file, char __user *buf, size_t len, loff_t *offset)
{
    // 内核态缓冲区
    char k_buf[32] = "Hello Kernel Driver!\n";
    int ret;

    // 核心:用户态与内核态内存隔离,必须使用专用拷贝函数
    // 禁止直接使用strcpy、memcpy跨空间拷贝
    ret = copy_to_user(buf, k_buf, sizeof(k_buf));
    if (ret > 0)
    {
        // 内核打印,替代用户态printf
        printk(KERN_ERR "copy_to_user failed!\n");
        return -EFAULT;
    }

    return sizeof(k_buf);
}

// 驱动文件操作结构体,绑定回调函数
static struct file_operations dev_fops = {
    .owner = THIS_MODULE,
    .read  = dev_read,
};

// 驱动初始化入口
static int __init drv_init(void)
{
    int ret;
    // 申请字符设备号
    ret = alloc_chrdev_region(&dev_num, 0, 1, DEV_NAME);
    if (ret < 0)
    {
        printk(KERN_ERR "设备号申请失败\n");
        return ret;
    }
    // 创建设备类、自动在/dev下生成设备文件
    dev_class = class_create(THIS_MODULE, CLASS_NAME);
    dev_device = device_create(dev_class, NULL, dev_num, NULL, DEV_NAME);

    printk(KERN_INFO "===== 驱动加载成功 =====\n");
    return 0;
}

// 驱动卸载出口
static void __exit drv_exit(void)
{
    // 反向释放内核资源
    device_destroy(dev_class, dev_num);
    class_destroy(dev_class);
    unregister_chrdev_region(dev_num, 1);

    printk(KERN_INFO "===== 驱动卸载成功 =====\n");
}

// 注册模块入口与出口
module_init(drv_init);
module_exit(drv_exit);

2. 编译配置 Makefile

makefile 复制代码
obj-m += chr_dev.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) clean

五、用户态测试程序(应用层交互)

user_test.c

纯标准C编写,模拟业务程序调用驱动,打通应用→内核链路:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#define DEV_PATH "/dev/mychrdev"

int main(void)
{
    int fd;
    char recv_buf[64] = {0};
    ssize_t ret;

    // 打开设备文件,触发open系统调用
    fd = open(DEV_PATH, O_RDONLY);
    if (fd < 0)
    {
        perror("open 设备失败");
        return -1;
    }
    printf("✅ 成功打开设备:%s\n", DEV_PATH);

    // 读取驱动数据,触发read系统调用
    ret = read(fd, recv_buf, sizeof(recv_buf));
    if (ret < 0)
    {
        perror("read 读取失败");
        close(fd);
        return -1;
    }

    printf("✅ 读取内核驱动数据:\n%s\n", recv_buf);
    close(fd);
    return 0;
}

六、完整实操部署命令

1. 编译驱动

bash 复制代码
make

2. 加载内核模块

bash 复制代码
sudo insmod chr_dev.ko

3. 查看内核日志(查看printk输出)

bash 复制代码
dmesg | tail

4. 编译用户态测试程序

bash 复制代码
gcc user_test.c -o user_test

5. 运行测试程序

bash 复制代码
sudo ./user_test

6. 简易测试(直接读取设备)

bash 复制代码
cat /dev/mychrdev

7. 卸载驱动 + 清理环境

bash 复制代码
sudo rmmod chr_dev
make clean

七、用户态 → 内核态 完整调用流程

  1. 用户态程序执行
    open /dev/mychrdevread() 运行在Ring3受限模式;
  2. C库触发系统调用
    glibc 内部通过汇编指令,主动切换CPU权限,陷入内核态
  3. 内核匹配设备
    根据主次设备号,匹配我们编写的字符设备驱动;
  4. 执行驱动回调函数
    内核自动调用 file_operations 中绑定的 dev_read 函数;
  5. 内核数据拷贝
    通过 copy_to_user 将内核数据安全发送到用户态缓冲区;
  6. 切换回用户态
    系统调用结束,CPU降级为Ring3,应用层拿到数据正常运行。

八、内核API 与 用户态C库 对照表

业务场景 用户态C库函数 内核态专用API
日志打印 printf printk
内存分配 malloc / free kmalloc / kfree
数据读写 fopen / read / write 设备文件操作 + 内核fops
跨空间拷贝 memcpy / strcpy copy_to_user / copy_from_user
延时等待 sleep / usleep msleep / udelay

九、学习第一天核心总结

  1. 分层思想:用户态负责业务,内核态负责底层硬件与资源管理
  2. 隔离机制:内存、权限、运行环境完全隔离,不能跨层混用接口;
  3. 驱动本质:驱动是内核的一部分,必须使用内核原生API;
  4. 开发禁忌:禁止在驱动中使用C库、禁止直接越界访问内存、禁止中断上下文睡眠;
  5. 交互核心:应用通过系统调用进入内核,驱动通过专用函数完成数据交互。
相关推荐
ADHD多动联盟2 小时前
专注力障碍是什么?主要有哪几点影响孩子的学习与社交能力?
学习·学习方法·玩游戏
计算机安禾2 小时前
【Linux从入门到精通】第23篇:条件判断——让脚本拥有“大脑”
linux·运维·服务器
feng_you_ying_li2 小时前
linu之进程的程序替换与shell基本实现的基本版本
linux
知识分享小能手2 小时前
R语言入门学习教程,从入门到精通,R语言网格绘图系统(ggplot2)- 完整知识点与案例代码(3)
开发语言·学习·r语言
念恒123062 小时前
进程控制---进程程序替换
linux·c语言
GISer_Jing2 小时前
从“工具应用”到“系统重构”:AI时代前端研发的范式转移与哲学思辨
前端·人工智能·学习
小夏子_riotous2 小时前
Docker学习路径——10、Docker Compose 一站式编排:从入门到生产级部署
linux·运维·服务器·docker·容器·centos·云计算
zhangrelay2 小时前
三分钟云课实践速通--概率统计--python版
linux·开发语言·笔记·python·学习·ubuntu
东风破1372 小时前
DM达梦数据库体系结构学习记录
数据库·学习