Linux内核源码解读

通过qemu运行一个最小系统

下载linux内核源码和busybox源码

我下载的版本是Linux内核4.9.229版本 linux源码下载地址 我下载的busybos源码版本是1.36.1版本 busybox源码下载地址

# 下载最新稳定版(如 1.36.1)
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xf busybox-1.36.1.tar.bz2
cd busybox-1.36.1
make menuconfig #可以根据需求做一些特定的修改
make 
make install

make install 后我们的busybox的内容是不完整的,可以去修改busybox的配置。 参考b站 简说linux b站视频链接 将源码编译后用qemu运行。

通过qemu启动内核

qemu启动脚本:vim start.sh

qemu-system-x86_64 -kernel ./linux-4.9.229/arch/x86_64/boot/bzImage  -initrd ./busybox-1.36.1/rootfs.img.gz  -app
end "root=/dev/ram init=/linuxrc" -serial file:output.tx

内核源码各个目录的解读

核心功能目录

  1. arch
  1. kernel
  1. mm
  1. init

驱动与设备

  1. drivers
  1. block

文件系统与网络

  1. fs
  1. net

工具与辅助模块

  1. lib
  1. scripts
  1. tools

安全与虚拟化

  1. security
  1. virt

文档与示例

  1. Documentation
  1. samples

其他重要目录

可以结合Documentation/和在线内核文档(如 kernel.org/doc)获得详细内容。

Linux网络设备驱动开发思路指南

loopback设备:

loopback设备的功能

核心开发步骤

1. 注册网络设备

2. 实现网络设备操作

3. 注册网络协议栈

4. 处理网络事件

5. 卸载和清理

6. 用户空间接口

7. 测试和调试

Linux中断

编写一个最简单的字符设备驱动

核心开发步骤

1. 设备号管理

2. 设备对象初始化

3. 操作接口绑定

4. 系统注册流程

  1. 初始化设备对象(cdev_init)
  2. 设置所属模块(cdev.owner = THIS_MODULE)
  3. 添加至系统(cdev_add)

5. 用户空间接口

一般需要注册设备号,分配cdev内存(cdev是字符设备的结构体),如果需要自动创建设备节点,还需要创建calss和device,如果手动创建设备节点则用mknod,然后实现file_operations一些操作绑定,初始化cdev对象。

手动创建设备节点则需要:mknod /dev/hello c 232 0 主要需要体现设备节点名称,以便于应用程序打开调用,c为字符设备描述,232为主设备号,0为次设备号。

一般就是在应用层调用 file 的open、read、write等等操作。

应用层调用wite函数->c库write函数->系统调用,内核的wite会判断你的file_poerations 的write函数是否定义,定义了则调用你写的哪个驱动wirte函数。

字符设备驱动一般放在drivers/char 文件夹下面,把你编写的字符设备驱动.c拷贝到当前目录,然后去修改Kconig 文件,修改很简单,可以复制粘贴Kconfig里面已有的内容: sh config_HELLO tristate "hello_device" #tristate三个选项 y m n bool是两个选项: y,n default y help hello device

然后通过make menuconfig就可以看到hello_device在内核源码树里面了。

编译之前,需要在makefile文件里面假如obj-$(config_HELLO) +=hello_Dev.o 自动读取config_HELLO 是为 y、 n、m等参数。

说白了驱动文件分为动态加载和静态加载,静态加载就是驱动文件写入内核中,不需要手动insmod 加载驱动.ko文件。

内核空间和用户空间的概念以及内核空间和用户空间的内存拷贝

2^32 bit = 2^22 Byte = 2^12 MB = 2^2 GB = 4GB

系统的用户空间从地址的 0x00000000 - 0xBFFFFFFF(3GB)

系统的内核空间从地址的 0xc0000000 - 0xFFFFFFFF(1GB)

当然这大小可以选择配置,通过配置PAGE_OFFSET

x86,用户运行在Ring3模式,内核运行在Ring0模式 arm,用户运行在usr模式,内核运行在svc模式

当用户调用系统调用的时候,会触发一个中断,从而进入到内核。这些其实都在用户空间的进程中进行,而且此时会被硬件中断打断。

write驱动函数使用copy_from_user函数,需要在内核驱动函数里面的多增加一个参数是用户空间buf缓冲区的地址。

read驱动函数使用copy_to_user函数

这两个函数看起来确实挺简单,但是底层逻辑来讲,是先通过线性地址读取物理地址的内容,然后拷贝在通过映射到线性地址给用户,如果内存中不存在,需要去磁盘寻找,效率会大打折扣。

x86段页式内存管理和页表映射机制

逻辑地址 ---(段管理) --> 线性地址 ---(页管理)-->物理地址

在linux内核里面的段选择Segment Selcetor设置为了0,也就是其实逻辑地址就是线性地址。

关于二级页表的映射

线性地址是32位,高10位为页表目录,中10位位页表项,低12位为偏移量,先从页表目录索引,然后再索引页表项,然后得到物理块编号 + offset 就可以得到 物理地址。

每个进程的页表目录不同,就不会造成不同进程之间有访问其他数据的情况了。

linux内核同步场景以及解决之道

对某个设备的访问,只允许一个进程进行访问,需要对这个资源进行互斥访问,访问资源的代码称为临界区。因此需要用到信号量semaphore。通过down(加锁) 和 up(释放锁)。

在其他进程想要访问临界区代码的时候,如果没有获取到锁则会休眠

主要看down 和 up 的实现,list_head 用于连接所有等待信号量的进程 semaphore_waiter表示一个正在等待信号量的进程。

// include/linux/semaphore.h
struct semaphore {
    raw_spinlock_t      lock;       // 保护信号量的自旋锁
    unsigned int        count;      // 资源计数器
    struct list_head    wait_list;  // 等待队列
};

// 等待队列节点
struct semaphore_waiter {
    struct list_head list;
    struct task_struct *task;       // 等待的进程
    bool up;                       // 是否已被唤醒
};

// kernel/locking/semaphore.c
void sema_init(struct semaphore *sem, int val)
{
    static struct lock_class_key __key;
    *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
    lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}

void down(struct semaphore *sem)
{
    unsigned long flags;
    
    // 尝试快速获取
    if (likely(sem->count > 0)) {
        sem->count--;
        return;
    }
    
    raw_spin_lock_irqsave(&sem->lock, flags);
    if (likely(sem->count > 0)) {
        sem->count--;
        raw_spin_unlock_irqrestore(&sem->lock, flags);
        return;
    }
    
    // 资源不足,进入等待
    __down(sem);
    raw_spin_unlock_irqrestore(&sem->lock, flags);
}

static noinline void __sched __down(struct semaphore *sem)
{
    __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}

static inline int __sched __down_common(struct semaphore *sem, long state,
                                long timeout)
{
    struct semaphore_waiter waiter;
    
    // 将当前任务加入等待队列
    waiter.task = current;
    waiter.up = false;
    list_add_tail(&waiter.list, &sem->wait_list);
    
    for (;;) {
        if (signal_pending_state(state, current))
            goto interrupted;
        if (unlikely(timeout <= 0))
            goto timed_out;
            
        __set_current_state(state);
        raw_spin_unlock_irq(&sem->lock);
        timeout = schedule_timeout(timeout);  // 让出CPU
        raw_spin_lock_irq(&sem->lock);
        
        if (waiter.up)  // 被up操作唤醒
            return 0;
    }
    
 timed_out:
    list_del(&waiter.list);
    return -ETIME;
    
 interrupted:
    list_del(&waiter.list);
    return -EINTR;
}

void up(struct semaphore *sem)
{
    unsigned long flags;
    raw_spin_lock_irqsave(&sem->lock, flags);
    
    if (likely(list_empty(&sem->wait_list))) {
        sem->count++;  // 无等待者,直接增加计数
    } else {
        __up(sem);     // 唤醒等待者
    }
    
    raw_spin_unlock_irqrestore(&sem->lock, flags);
}

static noinline void __sched __up(struct semaphore *sem)
{
    // 获取等待队列中的第一个任务
    struct semaphore_waiter *waiter = list_first_entry(
        &sem->wait_list, struct semaphore_waiter, list);
        
    list_del(&waiter->list);      // 从队列移除
    waiter->up = true;            // 标记为已唤醒
    wake_up_process(waiter->task); // 唤醒任务
}

按照我自己学习和看书的理解:

down操作,就是如果没有了信号量,则需要让需要执行的任务让出CPU,就是将需要等待执行的任务链表放在信号量存储的等待队列里面,然后呢up操作的时候检查信号量的等待队列是否任务链表,有的话,将任务链表唤醒

如果一些资源非常复杂,不可以让任务进行休眠,可以用原子变量实现,atomic_t 原子操作不会被cpu、打断,也不会被调度程序打断,常用的有atomic_dec_and_test(v--)和atomic_inc(v++)。

因为原子变量用到了汇编,因此先分析一下arm内核源码

```c static inline void atomic_add(int i, atomic_t *v) { unsigned long tmp; // 用于存储strex指令的结果(成功0/失败1) int result; // 存储从内存加载的当前值

// 预取指令:提示CPU即将写入该内存地址,优化缓存性能
prefetchw(&v->counter);

// 内联汇编开始
__asm__ __volatile__(
    "@ atomic_add\n"     // 汇编注释,表明这是原子加操作
    
    // 循环标签:如果存储失败将跳转回这里重试
    "1: ldrex   %0, [%3]\n"   // 独占加载:%0 = result = *(%3 = &v->counter)
    "   add %0, %0, %4\n"      // 执行加法:result = result + %4(i)
    "   strex   %1, %0, [%3]\n" // 独占存储:尝试将result写回内存,%1(tmp)=存储结果(0成功/1失败)
    "   teq %1, #0\n"          // 测试tmp是否等于0
    "   bne 1b"                // 如果存储失败(tmp != 0),跳回标签1重试
    
    // 输出操作数
    : "=&r" (result),   // %0:绑定到result变量(寄存器约束,&表示早期修改)
      "=&r" (tmp),      // %1:绑定到tmp变量
      "+Qo" (v->counter) // %2:内存操作数,+表示读写,Q表示内存地址约束
    
    // 输入操作数
    : "r" (&v->counter), // %3:输入参数,v->counter的地址(寄存器约束)
      "Ir" (i)           // %4:输入参数,立即数i(Ir表示立即数或寄存器)
    
    // 破坏描述部分
    : "cc"              // 表示条件码寄存器被修改
);

} ```

这里总结一下信号量的特点:

1.用于进程和进程之间的同步

2.允许有多个进程进入临界区代码执行

3.进程获取不到信号量锁会陷入休眠,并让出cpu

4.被信号量锁保护的临界区代码允许睡眠

5.本质是基于进程调度器,UP(单核)和SMP(多核)下的实现无差异。

6.不支持进程和中断之间的同步

同样是以arm为例子

typedef struct {
#ifdef __AARCH64EB__  // 大端模式
    u16 next;
    u16 owner;
#else                 // 小端模式
    u16 owner;
    u16 next;
#endif
} __aligned(4) arch_spinlock_t;

char设备驱动到platform驱动的华丽转身

Platfrom相当于一个虚拟的总线驱动,就算不在硬件的总线,也可以通过platform进行管理。

使用Platform驱动在Linux设备驱动开发中具有两大核心优势:

  1. 硬件与驱动的解耦,驱动注册独立于硬件,通过platform_driver_register()注册纯软件逻辑。设备描述独立存储:硬件资源(地址/中断/时钟)在设备树(DTS)或ACPI表中定义。

  2. 跨平台移植便捷性,相同驱动适配不同硬件:仅需修改设备树,无需重写驱动。

那么相对于之前简易的字符设备驱动有什么区别呢?

就是将驱动函数的名字需要改变

hello_init() -> hellodev_probe()
hello_exit() -> hellodov_remove() 

然后多了 platform平台驱动driver结构体 和 平台设备device结构体。


static struct platform_driver hello_driver = {
    .probe = hellodev_probe,
    .remove =hellodov_remove,
        .driver = {
        .name = "hello-dev",
        .of_match_table = my_of_match, // 设备树支持
    },
};

struct platform_device hello_device = {
    .name = "hello-device",
    .id = -1,
    .num_resources = ARRAY_SIZE(hellodev_resources),
    .resource = hellodev_resources,
};

不过现在的device都放在了dts下面了

然后就是编写字符设备初始化

charDevInit
      // 注册平台驱动
    ret = platform_driver_register(&hello_driver);
    //然后匹配驱动的probe函数,执行驱动中的hellodev_probe

charDevExit
   // 注销平台驱动
    platform_driver_unregister(&my_platform_driver);
struct bus_type platform_bus_type = {
    .name       = "platform",
    .dev_groups = platform_dev_groups,
    .match      = platform_match, // 核心匹配函数
    .uevent     = platform_uevent,
    .pm     = &platform_dev_pm_ops,
};
EXPORT_SYMBOL_GPL(platform_bus_type);

static int platform_match(struct device *dev, struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);
    
    /* 1. 检查 driver_override 强制匹配 */
    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);
    
    /* 2. 设备树匹配(现代首选) */
    if (of_driver_match_device(dev, drv))
        return 1;
    
    /* 3. ACPI 匹配 */
    if (acpi_driver_match_device(dev, drv))
        return 1;
    
    /* 4. ID 表匹配 */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;
    
    /* 5. 名称直接匹配 */
    return (strcmp(pdev->name, drv->name) == 0);
}

//设备树匹配函数
static inline int of_driver_match_device(struct device *dev,
                    const struct device_driver *drv)
{
    return of_match_device(drv->of_match_table, dev) != NULL;
}
// 用户强制指定驱动
echo "my_driver" > /sys/bus/platform/devices/my_device/driver_override

```c // 设备树节点 my_device: my-device@12340000 { compatible = "vendor,device-1", "vendor,device-generic"; reg = <0x12340000 0x1000>; };

// 驱动中的匹配表 static const struct of_device_id my_of_match[] = { { .compatible = "vendor,device-1" }, { .compatible = "vendor,device-2" }, {} // 结束标记 }; MODULE_DEVICE_TABLE(of, my_of_match); ```

用于 x86 等 ACPI 系统

```c static int acpi_driver_match_device(struct device dev, struct device_driver drv) { const struct acpi_device_id *acpi_ids = drv->acpi_match_table;

if (!acpi_ids)
    return 0;

return acpi_match_device_ids(to_acpi_device(dev), acpi_ids) == 0;

} ```

```c // 驱动定义 ID 表 static const struct platform_device_id my_id_table[] = { { "device_v1", 0 }, // 匹配设备名为 "device_v1" { "device_v2", 0 }, {} // 结束标记 };

// 平台驱动结构 static struct platform_driver my_driver = { .driver = { .name = "generic_device", }, .id_table = my_id_table, // ID 表 .probe = my_probe, .remove = my_remove, }; ```

```c // 设备 struct platform_device my_device = { .name = "specific_device", // 必须与驱动名匹配 };

// 驱动 static struct platform_driver my_driver = { .driver = { .name = "specific_device", // 与设备名相同 }, .probe = my_probe, }; ```

页框和伙伴算法以及slab机制