内核开发总结

https://space.bilibili.com/646178510 学习记录

开发基础

7、linux内核编译过程

顶层根目录有个顶层makefile,各级源代码目录还有一个makefile,顶层makefile去include各子目录的makefile文件,从而把内核源码囊括起来。

9、内核空间和用户空间以及数据拷贝

image-20230324142511108

image-20230324143202467

image-20230324143236217

copy_from_user(addr_kernel, addr_user, len)

copy_to_user(addr_user, addr_kernel, len)

为什么不允许内核直接访问用户空间的数据呢?防止缺页?安全性?

10、x86页式内存管理和页表映射

image-20230324150530121

逻辑地址->线性地址->物理地址

分为两个阶段:段页式转换->页表映射,只是linux内核里段基址是0,也就是逻辑地址等于线性地址。

页表映射

以二级页表转换为例。

image-20230324151515349

(物理块就是物理页)

cr3->页目录表->二级页表->物理页地址->加上偏移就是物理地址。

11、内核同步场景

比如设备驱动的open,有的设备可以被多个进程访问,比如磁盘、网卡。

一个有问题的例子:

image-20230320152225655

出现问题:

image-20230320151950659

这个例子中,xxx_count就是一个共享资源,并发执行单元对共享资源的访问会引发竞态问题。需要使用内核同步方法来保护临界区。

12、semaphore使用

image-20230321132044807

1
2
3
4
struct semaphore sema;
sema_init(&sema, 1);
down(&sema);
up(&sema);

image-20230320152849524

13、semaphore内核实现

本质上是基于进程调度器实现的,UP和SMP实现无差异

结构体

image-20230320153001796

list_head是双向链表

初始化

image-20230320153134335

初始化一个结构体,然后赋值。

down

获取一个锁

image-20230320153327793

如果获取不到,进入down

image-20230320153617757

image-20230320153650943

image-20230320153801572

就是第二个进程进来后,会创建一个waiter,加入到sem的wait_list里。然后进入while 1,把当前这个进程设置为休眠,然后释放down函数里申请的自旋锁,让这个把当前的进程调度出去,等被唤醒后,看下waiter.up,为true就返回。

传入的state是进程的状态,task_uninterruptable,不可运行且不能被打断的。

up

down里检查waiter.up,就是在up里设置的。跟down是对应的

image-20230320154726370

image-20230320154801612

理一下,就是当资源不够,当前进程会休眠,休眠之前,这个task_struct会加入到这个sem的wait_list里面。

up时候,如果wait_list有进程,就把它拿出来,然后wake_up。

14、内核原子变量

为啥要有原子变量,比如刚刚的例子,只是要对一个open_count进行保护,使用信号量的开销太大了,信号量会让进程调度。也就是atomic适合针对int变量进行同步的场景。

image-20230320155750926

image-20230320155850515

atomic_dec_and_test就是原子减一 ,结果为0 返回 true,atomic_inc就是原子加一,不会被调度,不会被cpu打断。

15、atomic内核实现

实现跟体系结构相关。视频中讲的是arm下的,这里看x86的

16、spinlock使用

信号量不支持进程和中断之间的同步,但是spinlock可以。中断上下文不允许睡眠,适合spinlock。

image-20230321132337095

死等是用特殊的汇编指令实现的。

1
2
3
4
spinlock_t count_lock;
spin_lock_init(&count_lock);
spin_lock(&count_lock);
spin_unlock(&count_lock);

使用自旋锁的临界区代码尽可能少,不能调用睡眠或可能引起睡眠的函数。

image-20230321132833399

image-20230321133403833

17、spinlock内核实现(UP)

实现也跟体系结构相关。

结构体

封装了三层:spinlock,raw_spinlock,arch_spinlock_t(具体实现),多一层封装多一层灵活性。

image-20230321133546072

image-20230321134454452

image-20230321134227826

使用owner和next来保证先到先唤醒。

image-20230321134553611

初始化

就是对raw_lock进行初始化,owner和next都等于0

image-20230321140139665

读内核时候,check函数可以不仔细看,就是辅助函数

image-20230321140436955

image-20230321142653602

image-20230321142816505

spin_lock

image-20230321142947003

image-20230321143004515

_raw_spin_lock的实现有两种,分别对应UP和SMP的。

up的实现比较简单,关闭抢占,__LOCK就什么都没做了

在include/linux下

image-20230321143224851

image-20230321143243009

18、spinlock内核实现(SMP)

接上,区别在_raw_spin_lock的实现,在kernel/locking下

image-20230321143904162

image-20230321143915292

多核也是先关抢占,然后要同步其他cpu,最后是体系结构相关的代码。

19、spinlock、rw spinlock、seqlock、rcu机制比较

image-20230321145117991

image-20230321145356342

image-20230321145604286

lock更新后,引起其他cpu核的cache失效。性能满足不了需求,就提出了RCU机制。

image-20230321150258599

22、页框和伙伴算法和slab

image-20230321150541275

image-20230321151033466

image-20230321151053950

image-20230321151218975

image-20230321151301903

image-20230321151617952

23、内存管理和分配方法

image-20230321151835475

image-20230321151918293

kmem_cache_alloc会调用slab_aclloc。flags是运行睡眠还是不允许睡眠。

image-20230321155035163

给硬件分配的一般要求连续,用kmalloc

image-20230321155156658

24、kmalloc实现

kmalloc->__kmalloc-> __do_kmalloc

主体实现

kmalloc依赖slab分配器实现,slab又依赖伙伴系统实现。

image-20230321155904171

根据size找到对应的高速缓存,然后调用slab_alloc找到一个空闲的object,然后把它的指针返回。 kasan_kmalloc是内存调试用的,先忽略。

根据size获取kmem_cache(高速缓存)

kmem_cache

image-20230321160244676

换算成一个index,从kmalloc_caches数组里拿。

25、高速缓存和size的对应关系

26、虚拟地址空间管理

不同的区域用不同的vma管理

image-20230321162757962

image-20230321163215100

image-20230321163033269

27、虚拟地址空间管理的内核结构

image-20230321163442644

image-20230321163905628

image-20230321163916165

image-20230321164044371

对于线程来说,多个线程共享一个地址空间,也就是多个 task_struct 使用同一个 mm_struct 结构,所以执行 fork 时只需要把父进程的 mm 赋值给子进程的 mm;

28、mmap实现

pass

29、用户栈和内核栈

用户栈是vma来管理,另一个作为task_struct的一部分。

image-20230321164913884

我记得内核栈是8k。

image-20230321165143139

image-20230321165214904

image-20230321165349053

内核会有其他方式来保证内核栈不溢出。

thread_info是体系结构相关的,可以看下arm的:

image-20230321165505295

30、进程上下文和中断上下文

preempt_count

这个变量保存在thread_info里面

image-20230321170132612

32、软中断特点以及softirq注册和触发

image-20230324161544265

哪个cpu触发软中断,哪个cpu执行这个软中断处理函数

每个cpu都有一个__softirq_pending变量,所以只能看到本地cpu软中断的状态。

image-20230324161747607

33、do_softirq()的执行时机

ksoftirqd线程

每个cpu都会创建这么一个线程,ksoftird/%u

image-20230324152729109

image-20230324153043345

这里的代码解释下,local_irq_disable()关闭中断是为了保护数据,__do_softirq()里又把硬中断打开了,所以说在软中断的执行过程中是打开cpu中断的。

硬件中断返回irq_exit()

image-20230324153208352

如果本地cpu上有待处理的软中断,只有不处于中断上下文的时就会调用__do_softirq()。

为什么需要判断 !in_inerrupt() ?那是因为在某些时候,软中断被硬件中断抢占执行,硬中断退出时,尽管退出了硬中断上下文,但是仍处于软中断上下文中,这时候不应该嵌套执行软中断,因为硬中断函数返回之后会继续执行被打断的软中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline void invoke_softirq(void)
{
if (ksoftirqd_running(local_softirq_pending()))
return;

if (!force_irqthreads) {
...
__do_softirq();
#endif
} else {
wakeup_softirqd();
}
}

直接调用

例如在网络子系统,对实时性要求比较高,所以会直接调用do_softirq进行处理。

image-20230324153614633

参考

https://zhuanlan.zhihu.com/p/363225717

34、软中断的执行过程

image-20230324163149398

得到已经触发的软中断flag,然后一个个调它的软中断处理函数。

  • 不同软中断优先级如何体现

image-20230324162755709

  • ksoftirqd优先级高于普通进程

硬件中断返回时候,就会调用:

image-20230324162853793

这里几乎没什么条件,就会调用invoke_softirq

35、free memory和avaliable memory

free memory是完全没有被用到的内存,而Linux认为内存不用也是浪费,因此会尽量“多”地把内存用来做各种缓存,提高系统的性能。而available memory是应用程序认为可用内存数量,它是free memorybuffercache的总和。

追踪调试基础

1、linux追踪技术三大件

image-20230324164642462

前端工具:应用层工具。

tracing framework:把追踪的事件返回到用户空间

事件源:比如printk就是一种事件源

image-20230324165128388

2、事件源

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×