Linux之kernel(1)系统基础理论(3)
Linux之Kernel(1)系统基础理论(3)
Author: Once Day Date: 2025年2月6日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…
漫漫长路,有人对你微笑过嘛…
全系列文章可参考专栏: Linux内核知识_Once-Day的博客-CSDN博客
参考文章:
- openEuler OS知识连载 - 知乎
- 引言 - rCore-Camp-Guide-2024A 文档
- 计算机操作系统知识点总结(有这一篇就够了!!!)-CSDN博客
- 5万字、97 张图总结操作系统核心知识点 - 程序员cxuan - 博客园
- 【王道】操作系统 知识点总结(合集)【超详细!】 - Zyyyyyyyyy - 博客园
- 全方位剖析Linux操作系统,太全了-linux操作系统的特点
- OS发展史中各操作系统的形成、发展和特点_同时具备什么操作系统功能构成所谓前后系统,前后任务优先后台任务-CSDN博客
- 《现代操作系统》
文章目录
- Linux之Kernel(1)系统基础理论(3)
- 1. 内核线程开发
- 1.1 用户线程与内核线程的区别
- 1.2 内核调度
- 2. 中断服务
- 2.1 中断处理
- 2.2 下半部分机制
- 2.3 中断屏蔽
- 3. 内核定时器
- 4. 内核同步和互斥
- 5. 内核与用户空间通讯
- 5.1 netlink的特点
- 5.2 proc中的内容
1. 内核线程开发
1.1 用户线程与内核线程的区别
用户线程与内核线程的区别主要体现在以下几个方面:
- 线程栈的位置不同,内核线程的栈位于内核空间,而用户线程的栈位于用户空间。
- 代码和数据的位置不同,内核线程的代码和数据只存在于内核空间,用户线程则只能访问用户空间。
- 运行状态不同,内核线程始终运行在内核态,而用户线程只能运行在用户态。当用户线程需要执行一些特权指令或系统调用时,需要切换到内核态,由内核代其执行。
- 调度和管理方式不同,内核线程由内核负责调度和管理,用户线程一般由线程库或应用程序自行调度,内核不感知。内核线程拥有更高的优先级和特权。
尽管有诸多不同,用户线程和内核线程在运行的上下文方面是相似的,本质上都是一个可调度的指令流。
在Linux中创建和使用内核线程主要有以下几个步骤:
- 定义一个线程入口函数,原型为
int (*threadfn) (void *data)
。这个函数就是线程的主体,其返回值和参数会被内核使用。 - 通过
kthread_create
创建一个内核线程,但不立即运行。kthread_run
可以创建并启动内核线程。 - 调用
wake_up_process
显式启动由kthread_create
创建的内核线程。 - 通过
kthread_stop
停止一个内核线程。被停止的线程会在某个时刻检查kthread_should_stop
,如果返回true则退出。所以线程体要配合定期检查kthread_should_stop
。 - 已经退出的线程再被
kthread_stop
会导致调用者被挂起,因为kthread_stop
要等待线程退出。
对于 pthread 创建的用户线程,Linux 内核并不感知,线程的调度完全由 pthread 库负责。为了让内核感知,从而获得更好的调度效果,一般采用 1:1 的线程模型,即一个 pthread 线程绑定一个内核调度实体(light weight process,LWP)。当 pthread 线程阻塞时,内核可以调度其他 LWP 运行,从而提高整体性能。
综上所述,用户线程与内核线程在 Linux 下通过 1:1 的映射模型进行协作,pthread 负责管理用户线程,内核负责调度 LWP,从而让操作系统有效利用多处理器资源,为多线程应用提供高效的运行环境。
1.2 内核调度
用户线程可以处于以下状态:
-
R (TASK_RUNNING),可执行状态。 只有在该状态的进程才可能在CPU上运行。而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)。进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上运行
-
S (TASK_INTERRUPTIBLE),可中断的睡眠状态。 处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。这些进程的task_struct结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。
-
D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态。 与TASK_INTERRUPTIBLE状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号。
-
T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态。 向进程发送一个SIGSTOP信号,它就会因响应该信号而进入TASK_STOPPED状态。向进程发送一个SIGCONT信号,可以让其从TASK_STOPPED状态恢复到TASK_RUNNING状态
-
Z (TASK_DEAD EXIT_ZOMBIE),退出状态,进程成为僵尸进程。在退出过程中,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外。于是进程就只剩下task_struct这么个空壳,故称为僵尸(之所以保留task_struct,是因为task_struct里面保存了进程的退出码、以及一些统计信息。而其父进程很可能会关心这些信息)。子进程在退出的过程中,内核会给其父进程发送一个信号,通知父进程来“收尸”。父进程可以通过wait系列的系统调用来等待某个或某些子进程的退出,并获取它的退出信息。然后wait系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉。
-
X(TASK_DEAD EXIT_DEAD),退出状态,进程即将被销毁。
内核线程虽然运行在内核空间,但它们与用户线程一样,也需要被内核调度才能获得CPU时间。
内核线程只能处于以下三种状态之一:
- TASK_RUNNING(可执行状态),表示线程可以被调度到CPU上运行。
- TASK_INTERRUPTIBLE(可中断的睡眠状态),通常因为等待某个事件而睡眠。当事件发生时,线程会被唤醒进入TASK_RUNNING状态。
- TASK_UNINTERRUPTIBLE(不可中断的睡眠状态),与TASK_INTERRUPTIBLE类似,但不响应异步信号。
内核线程不会处于TASK_STOPPED、TASK_TRACED、EXIT_ZOMBIE或EXIT_DEAD状态,因为这些状态都与用户空间进程的生命周期管理相关,而内核线程的生命周期完全由内核控制。
2. 中断服务
2.1 中断处理
中断是计算机系统中一种非常重要的机制,它允许CPU暂停当前的工作,转而处理更高优先级的外部事件。这些事件通常来自于外部硬件设备,如键盘、鼠标、硬盘等。每个中断都有一个唯一的中断号(IRQ),操作系统根据这个号码来识别和处理相应的中断。
当一个中断发生时,CPU会立即停止当前的工作,保存现场(即当前的寄存器状态),然后跳转到一个预先定义好的中断服务例程(ISR)来处理这个中断。ISR是一段驻留在内存中的程序,它负责完成与该中断相关的所有工作,如读取硬件状态、发送响应信号等。
ISR有以下几个重要特点:
- 需要事先注册,将自己与特定的IRQ关联起来。这样当该IRQ的中断发生时,系统才知道调用哪个ISR。
- ISR运行在中断上下文中,没有关联的进程,因此不能执行任何可能导致睡眠的操作,如申请内存、等待信号量等。
- 在ISR执行期间,本地中断是被屏蔽的,即同一个CPU上不能再响应其他中断,以避免中断嵌套导致的复杂性。但其他CPU可以同时响应其他中断。
- Linux中的ISR是无需重入的,因为同一个中断在所有CPU上都会被屏蔽,直到ISR执行完毕。
ISR通常用于执行一些时间敏感、硬件相关、或不可中断的任务,如读取网卡的数据包、更新键盘状态等。为了最小化ISR的执行时间,减少对其他任务的影响,复杂的数据处理工作通常会延迟到中断后的下半部分(如软中断、tasklet等)中执行。
当ISR完成后,它会通过一个特殊的返回指令(如iret)恢复中断前的现场,并返回到原来的执行点继续执行。如果在中断期间有新的调度请求(如抢占),系统会在恢复现场后立即调用schedule()进行进程切换。
2.2 下半部分机制
在Linux内核中,中断处理程序(ISR)通常被分为上半部和下半部:上半部是中断发生时立即执行的部分,主要用于完成一些时间敏感的任务,如响应硬件、保存现场等;而下半部则用于处理一些不太紧急、但又必须完成的工作,如数据处理、状态更新等。通过将这些工作推迟到下半部执行,可以大大减少ISR的执行时间,提高系统的实时性和响应性。
Linux内核提供了三种不同的下半部机制: softirq、tasklet和workqueue。它们在实现和使用上有一些差异:
- softirq和tasklet运行在中断上下文中,不能执行任何可能导致阻塞的操作。它们的执行时机是在ISR返回后、在恢复被中断的进程之前。此时,本地中断已经重新打开,因此softirq和tasklet可以被新的中断打断。
- workqueue则运行在进程上下文中,由特定的内核线程(worker thread)执行。这些线程可以像普通进程一样睡眠和阻塞,因此workqueue适合执行一些可能阻塞的工作,如等待I/O、分配内存等。
从性能开销来看,softirq的开销最小,其次是tasklet,workqueue的开销最大。这是因为softirq和tasklet直接运行在中断上下文中,没有额外的线程切换开销,而workqueue需要在不同的线程之间切换。
从易用性来看,workqueue最容易使用,因为它提供了一个类似于进程的编程环境。tasklet次之,它提供了一些方便的API来管理任务的执行。softirq使用起来最复杂,需要手动管理各种数据结构。
从功能性来看,只有workqueue能够执行可能阻塞的操作,因为它运行在进程上下文中。如果一个任务需要睡眠或阻塞,就必须使用workqueue。
最后,从执行优先级来看,ISR的优先级最高,因为它直接响应硬件事件。其次是softirq和tasklet,它们在ISR之后、进程之前执行。最后是workqueue,它由普通的内核线程执行,优先级与其他进程相同。
2.3 中断屏蔽
在Linux内核中,中断屏蔽是一种非常重要的同步机制,用于保护关键的代码段不被中断打断,确保数据的一致性和正确性。Linux提供了两种级别的中断屏蔽:IRQ中断屏蔽和软中断控制。
IRQ中断屏蔽是指暂时关闭当前处理器的所有外部中断(包括时钟中断),使得当前的代码可以不被打断地执行下去。这种屏蔽是针对每个处理器独立的,不影响其他处理器的中断处理。
IRQ中断屏蔽的主要目的是避免外部中断对当前代码执行过程的抢占,确保关键操作的原子性。例如,在修改一个全局数据结构时,如果不屏蔽中断,可能在修改到一半时被中断打断,导致数据不一致。
但是,IRQ中断屏蔽也有其局限性。在多核系统中,它只能避免来自同一个处理器的中断,无法避免其他处理器对共享数据的并发访问。因此,在使用IRQ中断屏蔽时,还需要考虑其他的同步机制,如锁、内存屏障等。
Linux提供了以下几个接口来控制IRQ中断屏蔽:
local_irq_enable/local_irq_disable
:使能/屏蔽当前处理器的本地中断。local_irq_save/local_irq_restore
:屏蔽当前处理器的本地中断,并保存当前的中断状态,恢复之前保存的中断状态。in_irq
:判断当前是否处于中断服务例程(ISR)中。
软中断控制是指暂时禁止当前处理器执行软中断(softirq)和tasklet,使得当前的线程可以不被这些下半部打断。与IRQ中断屏蔽类似,软中断控制也是针对每个处理器独立的。
软中断控制的主要目的是避免softirq和tasklet对当前线程的抢占,提高线程的执行效率。例如,在执行一个时间敏感的任务时,如果不禁止软中断,可能会频繁地被打断,导致任务的延迟增加。
同样地,软中断控制也无法避免其他处理器对共享数据的并发访问,需要与其他同步机制配合使用。
Linux提供了以下几个接口来控制软中断:
local_bh_disable/local_bh_enable
:禁止/使能当前处理器的软中断。in_interrupt
:判断当前是否处于中断上下文中,包括硬中断(ISR)和软中断。
3. 内核定时器
在Linux内核中,定时器是一种非常重要的机制,用于在指定的时间点触发某个事件或执行某个任务。内核定时器通常基于硬件时钟,如RTC(实时时钟)、TSC(时间戳计数器)、PIT(可编程间隔定时器)、HPET(高精度事件定时器)等,这些硬件时钟会定期产生中断,内核根据这些中断来维护时间和调度定时器。
在内核中,有几个重要的术语和概念与定时器相关:
- HZ:表示每秒钟产生的时钟中断数,即时钟频率。这个值可以在内核配置时指定,常见的值有100、250、1000等。
- Tick:表示一次时钟中断,是内核的基本时间单位。一个tick的时长等于1/HZ秒。
- Jiffies:表示系统启动以来产生的时钟中断总数,即经过的tick数。Jiffies是一个32位或64位的无符号整数,会在每次时钟中断时加1,用于衡量相对时间。
内核提供了一些宏和函数来操作和转换jiffies,如time_after、time_before、time_in_range等用于比较jiffies值,msecs_to_jiffies、jiffies_to_msecs等用于在jiffies和毫秒之间转换。
在内核中,最常用的定时器是低精度定时器,它的精度为1个tick,即1/HZ秒。内核使用一个叫做timer_list的结构体来表示一个定时器,多个定时器通过链表连接起来,按照到期时间排序。
使用低精度定时器通常分为三个步骤:
- 初始化定时器,设置定时器的到期时间(expires)和回调函数(function),并初始化链表节点。
- 启动定时器,调用add_timer函数将定时器添加到内核的定时器链表中,定时器会在指定的时间到期。
- 修改或删除定时器,在定时器到期前,可以调用mod_timer函数修改定时器的到期时间,或调用del_timer函数将定时器从链表中删除。
需要注意的是,定时器的到期时间是一个绝对值(jiffies+N),而不是相对值。定时器的回调函数运行在软中断上下文中,因此不能执行任何可能阻塞的操作。低精度定时器通常是非周期性的,即只触发一次,如果需要周期性触发,需要在回调函数中重新设置定时器。
此外,在使用定时器时还需要注意一些事项,如在定时器到期或删除前不能释放定时器结构体,否则可能破坏内核的定时器链表;定时器回调函数运行时,定时器已经从链表中移除,因此不要在回调函数中再次修改该定时器等。
4. 内核同步和互斥
完成量(Completion)是一种轻量级的同步机制,用于在两个或多个执行单元(如线程、中断处理程序等)之间同步操作。它的典型使用场景是:一个或多个线程等待某个操作完成,而另一个线程在操作完成后通知等待的线程。
完成量使用一个称为"完成变量"的结构体(struct completion)来表示,其中包含了一个等待队列和一个完成标志。等待的线程调用wait_for_completion函数,将自己加入等待队列并阻塞;通知的线程调用complete函数,设置完成标志并唤醒等待队列中的所有线程。
完成量的优点是简单高效,不需要繁琐的加锁操作,适用于一次性的事件通知场景。但是,它不能用于互斥访问,也不能传递复杂的数据。
工作队列(Workqueue)是一种将任务推后执行的机制,可以将一些不急迫的工作从当前上下文(如中断处理程序)中移出,交给特定的内核线程后台处理。这样可以避免在关键路径上执行时间过长或阻塞的操作,提高系统的实时性和并发性。
工作队列使用一个称为"工作项"的结构体(struct work_struct)来表示一个推后执行的任务,其中包含了一个回调函数和一些参数。内核维护了一些工作者线程(worker thread),专门用于处理工作队列中的任务。添加工作项可以使用queue_work函数,将工作项加入到指定的工作队列中,由工作者线程异步执行。
工作队列的优点是异步执行,不阻塞当前上下文,而且可以并发处理多个任务。但是,它需要额外的内核线程,且工作项的执行时间不可控,不适合实时性要求高的场景。
线程同步(Thread Synchronization)是指在多个线程并发访问共享资源时,通过一些机制来协调它们的执行顺序,以避免竞争条件和数据不一致。Linux内核提供了多种线程同步机制,如互斥锁(mutex)、自旋锁(spinlock)、信号量(semaphore)、顺序锁(seqlock)等。
这些同步机制通过一些特定的数据结构和算法,来实现对共享资源的互斥访问或同步操作。例如,互斥锁使用一个锁变量和等待队列,通过原子操作来获取和释放锁;自旋锁使用一个原子变量,通过忙等待来获取锁;信号量使用一个计数器和等待队列,通过PV操作来控制资源的使用;顺序锁使用一个序号和读写计数器,通过比较序号来检测并发修改。
为什么要禁止内核抢占?
如果在自旋锁保护的代码执行过程中发送抢占,则可能另外一个进程会再次调用spinlock保护的这段代码,会发生死锁。
为什么要避免长期持有?
其他处理器处于忙等待,无法进行其他工作
为什么持有期间不允许挂起?
如果在自旋锁保护的代码中间睡眠,此时发生进程调度,则可能另外一个进程会再次调用spinlock保护的这段代码。而我们现在知道了即使在获取不到锁的“自旋”状态,也是禁止抢占的,而“自旋”又是动态的,不会再睡眠了,也就是说在这个处理器上不会再有进程调度发生了,那么死锁自然就发生了。
互斥量和自旋量对比:
应用场合(多核) | 互斥量or自旋锁 |
---|---|
临界区执行时间较快 | 优先选择自旋锁 |
临界区执行时间较长 | 优先选择互斥量 |
临界区可能包含引起睡眠的代码 | 不能选自旋锁,可以选择互斥量 |
临界区位于非进程上下文时, 此时不能睡眠 | 优先选择自旋锁,即使选择互斥量也只能用down_trylock非阻塞的方式 |
需要互斥的场景:
-
不同进程之间竞争临界资源。
-
进程与中断下半部之间竞争临界资源。
-
进程与中断上半部之间竞争临界资源。
-
不同中断的下半部之间竞争临界资源。
-
不同中断的上半部之间竞争临界资源。
-
中断上半部与下半部之间竞争临界资源。
互斥机制的选择:
相同上下文 | 不同上下文 | |
---|---|---|
相同处理器 | thread:互斥量 | thread & softirq:屏蔽软中断 ; softirq & hardirq :屏蔽本地中断 ; thread & hardirq :屏蔽本地中断 |
不同处理器 | thread:互斥量、自旋锁 ; softirq:spin_lock_bh irq /spin_lock_irqsave | thread & softirq:spin_lock_bh; softirq & hardirq :spin_lock_irqsave ; thread & hardirq :spin_lock_irqsave |
说明: thread – 进程上下文;irq – 中断上下文(ISR);softirq – 中断上下文(softirq/tasklet)
5. 内核与用户空间通讯
5.1 netlink的特点
Netlink是Linux内核提供的一种特殊的IPC(进程间通信)机制,用于在内核空间和用户空间之间进行双向的数据传输。与其他IPC机制(如管道、消息队列等)不同Netlink具有以下几个特点:
- Netlink使用Socket API进行通信,在用户空间表现为一个特殊的socket家族(AF_NETLINK),可以使用标准的socket编程接口进行操作,如socket()、bind()、sendmsg()、recvmsg()等。
- Netlink支持异步通信,即内核和用户空间可以独立地发送和接收数据,不需要严格的同步。这种异步性可以提高系统的并发性和实时性。
- Netlink支持双向通信,即不仅用户空间可以主动向内核发送请求,内核也可以主动向用户空间发送事件或通知。这种双向通信可以实现更加灵活和高效的内核-用户交互。
- Netlink支持多播通信,即一个消息可以同时发送给多个接收者。这种多播特性可以方便地实现事件订阅、状态同步等功能。
- Netlink预定义了32种消息类型(如NETLINK_ROUTE、NETLINK_KOBJECT_UEVENT等),每种类型对应一种特定的内核子系统或服务。用户空间可以根据需要选择不同的消息类型进行通信。
使用Netlink进行内核-用户通信的一般步骤如下:
- 在内核空间中,通过netlink_kernel_create()函数创建一个Netlink Socket,并指定相应的消息类型和回调函数。回调函数用于处理来自用户空间的请求或命令。
- 在用户空间中,通过socket()函数创建一个AF_NETLINK类型的Socket,并通过bind()函数绑定到一个本地地址。然后,可以使用sendmsg()函数向内核发送Netlink消息,使用recvmsg()函数从内核接收Netlink消息。
- 内核收到用户空间发来的Netlink消息后,会根据消息类型和具体内容,调用相应的回调函数进行处理。回调函数可以访问内核数据结构、执行特权操作等,并可以通过Netlink Socket向用户空间发送响应或通知。
- 用户空间收到内核发来的Netlink消息后,可以根据消息类型和具体内容进行解析和处理,如更新状态、触发事件等。
Netlink提供了一种灵活、高效、可扩展的机制,用于实现Linux内核与用户空间之间的双向通信。它广泛应用于系统管理、设备控制、网络配置等领域,如进程事件通知(Connector)、内核配置更改(ConfigFS)、网络路由管理(NETLINK_ROUTE)等。
5.2 proc中的内容
/proc
是Linux内核提供的一种特殊的文件系统,它以文件和目录的形式,动态地展示内核和系统的运行时信息。与普通的文件系统不同,/proc
不占用存储空间,而是内核在访问时动态生成相应的文件和目录。通过读写/proc
中的文件,用户空间可以方便地获取系统的各种信息,或者动态调整内核的某些配置。
/proc
目录包含了大量的子目录和文件,分别提供不同类型的系统信息:
系统信息:
/proc/cpuinfo
:提供处理器的详细信息,如型号、主频、缓存大小等。/proc/meminfo
:提供内存的详细信息,如总内存、可用内存、缓存、交换空间等。/proc/version
:提供内核版本、编译器版本、操作系统名称等信息。
系统配置信息:
/proc/sys
:提供了一些内核参数的配置接口,通过读写该目录下的文件,可以动态调整内核的某些行为,如TCP/IP参数、内存管理参数等。这些配置会立即生效,但在系统重启后失效。
硬件信息:
/proc/devices
:提供当前注册的字符设备和块设备的主设备号信息。/proc/bus
:提供系统总线(如PCI、USB等)的信息,包括设备列表、驱动程序等。
进程信息:
/proc/[pid]
:对于每一个进程,内核会在/proc下创建一个以进程ID命名的目录,其中包含了该进程的各种运行时信息。/proc/[pid]/fd
:列出该进程打开的所有文件描述符,每个描述符都是一个符号链接,指向实际的文件路径。/proc/[pid]/mem
:该进程的虚拟内存,可以通过open/read/lseek等系统调用访问,常用于调试器。/proc/[pid]/status
:该进程的各种状态信息,如进程ID、父进程ID、内存使用量、优先级等。/proc/[pid]/environ
:该进程的环境变量,以NULL字符分隔的字符串形式展示。
/proc的一个重要特点是,它提供的信息是动态生成的,反映了系统的实时状态。当用户空间读取/proc中的文件时,内核会实时提供最新的信息;当用户空间写入某些/proc文件时,内核会立即作出相应的行为调整。这种机制避免了频繁的系统调用,提高了数据访问的效率。
/proc
文件系统是Linux内核提供的一个重要的信息接口,它以一种统一、灵活、易用的方式,向用户空间开放了内核和系统的运行时信息。通过读写/proc
中的文件,我们可以方便地查看系统的各种状态,如CPU、内存、进程、设备等,也可以动态地调整内核的某些配置参数。
Once Day
也信美人终作土,不堪幽梦太匆匆......
如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!
(。◕‿◕。)感谢您的阅读与支持~~~