我下载的版本是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启动脚本: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
archx86、arm、arm64)。kernelsched/)、信号处理(signal.c)、系统调用(sys.c)等。mmpage_alloc.c)、虚拟内存(vmalloc.c)、页表管理(pgtable.c)等。initmain.c包含内核初始化入口(start_kernel)。initramfs)。driverschar/(字符设备)、pci/、usb/、net/等。i2c/、spi/、platform/。blockdeadline-iosched.c)、通用块设备驱动。fsext4/、btrfs/、proc/、sysfs/等。vfs/目录下的通用文件操作接口。netipv4/、ipv6/)、TCP/UDP、Socket层。ethernet/、wireless/、mac80211/等。libscriptsKconfig解析、Makefile生成)。toolsperf/)、调试工具。securityvirtDocumentationsamplesinclude:内核头文件(分为架构相关头文件asm/和通用头文件linux/)。ipc:进程间通信机制(如共享内存、信号量、消息队列)。crypto:加密算法实现(如AES、SHA系列)。sound:音频子系统与驱动。usr:早期用户空间支持(如initramfs构建)。可以结合Documentation/和在线内核文档(如 kernel.org/doc)获得详细内容。
alloc_netdev或register_netdev分配设备号和初始化网络设备结构体。netdev_init用于初始化网络设备结构体。open和stop函数,处理设备的打开和关闭。hard_start_xmit函数,处理数据包的发送。set_rx_mode和netif_rx函数,处理数据包的接收。dev_add将网络设备添加到协议栈中。change_mtu、do_ioctl等函数,处理设备状态变化和用户空间请求。tx_timeout函数,处理发送超时等错误情况。unregister_netdev和free_netdev函数,用于卸载驱动和释放资源。udev规则自动创建设备节点,或手动使用mknod命令创建。ifconfig或ip命令配置网络设备,如IP地址、子网掩码等。dmesg、tcpdump等工具进行调试和分析。alloc_chrdev_regionregister_chrdev_regioncdev_alloc) vs 静态(struct cdev)open和releaseread/write/ioctl等cdev_init)cdev.owner = THIS_MODULE)cdev_add)mknod命令创建设备节点自动创建:通过class_create+device_create
编写驱动
一般需要注册设备号,分配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函数
这两个函数看起来确实挺简单,但是底层逻辑来讲,是先通过线性地址读取物理地址的内容,然后拷贝在通过映射到线性地址给用户,如果内存中不存在,需要去磁盘寻找,效率会大打折扣。
逻辑地址 ---(段管理) --> 线性地址 ---(页管理)-->物理地址
在linux内核里面的段选择Segment Selcetor设置为了0,也就是其实逻辑地址就是线性地址。
关于二级页表的映射
线性地址是32位,高10位为页表目录,中10位位页表项,低12位为偏移量,先从页表目录索引,然后再索引页表项,然后得到物理块编号 + offset 就可以得到 物理地址。
每个进程的页表目录不同,就不会造成不同进程之间有访问其他数据的情况了。
对某个设备的访问,只允许一个进程进行访问,需要对这个资源进行互斥访问,访问资源的代码称为临界区。因此需要用到信号量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.不支持进程和中断之间的同步
spinlock的说明和使用方法
自旋锁是一种死等机制,spinlock自旋锁一次只能有一个程序进入临界区,其他执行单元都是在死等,就算说spilock会一直占用cpu,所以spinlock执行时间会很短,因为不睡眠,自旋锁可以在中断上下文中进行。
spinlock调用自旋锁,spinunlock释放自旋锁。使用自旋锁执行的函数不能太长。
自旋锁常用于 进程与进程、和本地软中断、和本地硬中断的同步。由于自旋锁是一种特殊的机制,自选锁有一个spin_trylock去尝试获取锁,获取不到就不死等。
spinlock内核源码(UP版)
同样是以arm为例子
typedef struct {
#ifdef __AARCH64EB__ // 大端模式
u16 next;
u16 owner;
#else // 小端模式
u16 owner;
u16 next;
#endif
} __aligned(4) arch_spinlock_t;Platfrom相当于一个虚拟的总线驱动,就算不在硬件的总线,也可以通过platform进行管理。
使用Platform驱动在Linux设备驱动开发中具有两大核心优势:
硬件与驱动的解耦,驱动注册独立于硬件,通过platform_driver_register()注册纯软件逻辑。设备描述独立存储:硬件资源(地址/中断/时钟)在设备树(DTS)或ACPI表中定义。
跨平台移植便捷性,相同驱动适配不同硬件:仅需修改设备树,无需重写驱动。
那么相对于之前简易的字符设备驱动有什么区别呢?
就是将驱动函数的名字需要改变
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);源码角度分析platform设备和驱动的匹配原则
platform 是一条虚拟总线,可以将一些设备放置在该虚拟设备总线上,设备为 platform_device,要操作这些设备需要使用匹配的驱动,驱动为 platform_driver,当执行 insmod 时,驱动会在总线上查找与其对应的设备,查找成功后执行 probe 动作进行设备初始化,当驱动卸载后执行 remove 函数,进行退出动作。
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, }; ```
页框
内存中的内存块用struct page 描述,一般大小位4K
内核对于有些场景,需要申请大小不定的连续页框。所以需要用伙伴算法进行分配。
伙伴算法
怎么分配的呢:首先将linux内核中的空闲页框分组为11个链表,每个链表中的页框块是固定的 第i个链表中每个页框块都包含2的i次方个连续页。
就比如这个链表数组,第一个链表中包含了2^0 = 1个连续的页框,第4个链表包含了2^4个连续 的页框。
最大可申请的连续物理块是2^10 = 1024 个的连续页框,一个页框是4k,那么可以申请4M大小。
内存分配的逻辑无非就是查看链表里面是否有大小满足的,不满足则继续往下找,如果被占用也继续往 下找,找到一个可以分割一部分页框给这个任务的。 释放内存的逻辑则是判断这个连续的内存他的一些 页框有没有被释放,被释放了则合并。