多任务功能
多任务 |
---|
进程 |
线程或任务 |
同步 |
调度器 |
中断核心 |
CPU特定 |
Linux内核是一个抢占式的多任务操作系统。作为一个多任务操作系统,它允许多个进程共享处理器(CPU)和其他系统资源。每个CPU一次只执行一个任务。但是,多任务允许每个处理器在执行的任务之间切换,而无需等待每个任务完成。为此,内核可以随时暂时中断处理器正在执行的任务,并将其替换为另一个任务,该任务可以是新的任务或之前挂起的任务。涉及正在运行的任务交换的操作称为上下文切换。
进程是正在运行的用户空间程序。内核在函数run_init_process id中使用kernel_execve id启动第一个进程/sbin/init。进程占用系统资源,如内存、CPU时间。系统调用sys_fork id和sys_execve id用于从用户空间创建新进程。进程使用sys_exit id系统调用退出。
Linux从Unix继承了其基本的进程管理系统调用(⚲ API ↪ ⚙️ 实现)
man 2 fork ↪ kernel_clone id通过复制调用它的进程来创建一个新进程。
man 2 _exit ↪ do_exit id“立即”终止调用进程。属于该进程的任何打开的文件描述符都将关闭。
man 2 wait ↪ kernel_waitid id挂起调用进程的执行,直到其子进程之一终止。
man 2 execve ↪ do_execve id在当前进程的上下文中运行可执行文件,替换以前的可执行文件。此系统调用由libc的函数族使用man 3 exec
Linux通过其自己的系统调用man 2 clone增强了传统的Unix进程API。Clone创建一个子进程,该子进程可能与父进程共享其执行上下文的一部分。它通常用于实现线程(尽管程序员通常会使用更高级别的接口,如man 7 pthreads,它是在clone之上实现的)。
PID - 定义为pid_t id的进程标识符是唯一的顺序号。man 1 ps -A列出当前进程。系统调用man 2 getpid ↪ task_tgid_vnr id返回当前进程的PID,在内部称为TGID - 线程组ID。一个进程可以包含多个线程。man 2 gettid ↪ task_pid_vnr id返回线程ID。在内部历史上称为PID。⚠️警告:混淆。用户空间PID≠内核空间PID。man 1 ps -AF列出当前进程和线程作为LWP。对于单线程进程,所有这些ID都相等。
⚲ API
⚙️ 内部
- task_structid
- pid_typeid
- kernel/fork.csrc
- syscalls
- man 2 set_tid_address – 设置指向线程ID的指针
- man 2 fork – 创建子进程
- man 2 vfork – 创建子进程并阻塞父进程
- man 2 clone – 创建子进程
- man 2 unshare – 取消关联进程执行上下文的部分内容
- kernel/sys.csrc
- syscalls
- man 2 prctl – 对进程或线程进行操作
- kernel/pid.csrc
- syscalls
- man 2 pidfd_open – 获取引用进程的文件描述符
- man 2 pidfd_getfd – 获取另一个进程的文件描述符的副本
- syscalls
- man 2 pidfd_open – 获取引用进程的文件描述符
- man 2 pidfd_getfd – 获取另一个进程的文件描述符的副本
- kernel/exit.csrc
- syscalls
- man 2 exit – 终止调用进程
- man 2 exit_group – 退出进程中的所有线程
- man 2 waitid – 等待进程状态更改
- man 2 waitpid – 等待进程状态更改
📖 参考
进程间通信 (IPC) 特指操作系统提供的一系列机制,允许其管理的进程之间共享数据。实现 IPC 的方法被划分为不同的类别,这些类别根据软件需求(例如性能和模块化需求)以及系统环境而有所不同。Linux 从 Unix 继承了以下 IPC 机制:
信号 (⚲ API ↪ ⚙️ 实现)
- man 2 kill 向进程发送信号
- man 2 tgkill ↪ do_tkill id 向线程发送信号
- man 2 process_vm_readv ↪ process_vm_rw id - 进程地址空间之间零拷贝数据传输
🔧 待补充:man 2 sigaction man 2 signal man 2 sigaltstack man 2 sigpending man 2 sigprocmask man 2 sigsuspend man 2 sigwaitinfo man 2 sigtimedwait
- 匿名管道和命名管道 (FIFO) man 2 mknod ↪ do_mknodat id S_IFIFO id 参见零拷贝
- Express 数据路径 PF_XDP id
- Unix 域套接字 PF_UNIX id
- 内存映射文件 man 2 mmap ⤑ ksys_mmap_pgoff id
- Sys V IPC
- 消息队列
- 信号量
- 共享内存:man 2 shmget,man 2 shmctl,man 2 shmat,man 2 shmdt
📖 参考
在 Linux 内核中,“线程”和“任务”几乎是同义词。
💾 历史:直到 2.6.39 版本,内核模式只有一个线程,由大内核锁保护。
⚲ API
- linux/sched.h inc - 调度器主要 API
- arch/x86/include/asm/current.hsrc
- current id 和 get_current id () 返回当前 task_struct id
- uapi/linux/taskstats.h inc 每个任务的统计信息
- linux/thread_info.hinc
- 函数 current_thread_info id() 返回 thread_info id
- linux/sched/task.h inc - 调度器与各种任务生命周期(fork()/exit())功能之间的接口
- linux/kthread.h inc - 创建和停止内核线程的简单接口,无需复杂操作。
- kthread_run id 创建并唤醒线程
- kthread_createid
⚙️ 内部
调度器 是操作系统中决定在特定时间点运行哪个进程的部分。它通常具有暂停正在运行的进程、将其移至运行队列末尾并启动新进程的能力。
活动进程被放置在一个名为运行队列或runqueue的数组中 - rq id。运行队列可能包含每个进程的优先级值,调度器将使用这些值来确定接下来运行哪个进程。为了确保每个程序都能公平地共享资源,每个程序都会运行一段时间(时间片),然后暂停并放回运行队列。当一个程序停止以让另一个程序运行时,运行队列中优先级最高的程序将被允许执行。当进程请求睡眠、等待资源可用或被终止时,也会从运行队列中移除。
Linux 使用 完全公平调度器 (CFS),这是第一个在通用操作系统中广泛使用的公平排队进程调度器的实现。CFS 使用一个经过充分研究的经典调度算法,称为“公平排队”,最初是为数据包网络发明的。CFS 调度器的调度复杂度为 O(log N),其中 N 是运行队列中任务的数量。选择任务可以在恒定时间内完成,但任务运行结束后重新插入需要 O(log N) 次操作,因为运行队列是使用红黑树实现的。
与之前的O(1) 调度器不同,CFS 调度器的实现不是基于运行队列。相反,红黑树实现了未来任务执行的“时间线”。此外,调度器使用纳秒级粒度的计量,这是为单个进程分配 CPU 时间的原子单位(因此使得之前的时间片概念变得多余)。这种精确的知识也意味着不需要特定的启发式方法来确定进程的交互性,例如。
与旧的 O(1) 调度器一样,CFS 使用一个称为“睡眠公平性”的概念,它将睡眠或等待的任务视为与运行队列上的任务等效。这意味着大部分时间都在等待用户输入或其他事件的交互式任务,在需要时可以获得同等比例的 CPU 时间。
调度算法使用的数据结构是红黑树,其中节点是特定于调度器的结构,称为sched_entity id。这些结构派生自通用task_struct进程描述符,并添加了调度器元素。这些节点以纳秒为单位的处理器执行时间为索引。每个进程还会计算一个最大执行时间。此时间基于“理想处理器”将在所有进程之间平均分配处理能力的理念。因此,最大执行时间是进程等待运行的时间除以进程总数,换句话说,最大执行时间是进程在“理想处理器”上预期运行的时间。
当调用调度器来运行新进程时,调度器的操作如下:
- 选择调度树的最左节点(因为它将具有最短的已执行时间),并将其发送执行。
- 如果进程只是简单地完成执行,则将其从系统和调度树中移除。
- 如果进程达到其最大执行时间或以其他方式停止(自愿或通过中断),则根据其新的已执行时间将其重新插入调度树。
- 然后将从树中选择新的最左节点,重复迭代。
如果进程花费大量时间休眠,则其已执行时间值较低,当它最终需要时会自动获得优先级提升。因此,此类任务不会获得比持续运行的任务更少的处理器时间。
CFS 的替代方案是 Con Kolivas 创建的 Brain Fuck Scheduler (BFS)。与其他调度器相比,BFS 的目标是提供一个具有更简单算法的调度器,不需要调整启发式算法或调整参数以使性能适应特定类型的计算工作负载。
Con Kolivas 还维护着另一个 CFS 的替代方案,即 MuQSS 调度器。[1]
Linux 内核包含不同的调度器类(或策略)。目前默认使用的完全公平调度器是 SCHED_NORMAL id 调度器类,也称为 SCHED_OTHER。内核还包含另外两个类 SCHED_BATCH id 和 SCHED_IDLE id,以及另外两个实时调度类,名为 SCHED_FIFO id(实时先进先出)和 SCHED_RR id(实时循环),以及后来添加的被称为 SCHED_DEADLINE id 的第三个实时调度策略,该策略实现了 最早截止日期优先算法 (EDF)。任何实时调度器类都优先于任何“正常”(即非实时)类。调度器类是通过 man 2 sched_setscheduler ↪ do_sched_setscheduler id 系统调用进行选择和配置。
在调度器中正确平衡延迟、吞吐量和公平性是一个开放性问题。[1]
⚲ API
- man 1 renice – 正在运行的进程的优先级
- man 1 nice – 以修改后的调度优先级运行程序
- man 1 chrt – 操作进程的实时属性
- man 2 sched_getattr ↪ sys_sched_getattr id – 获取调度策略和属性
- linux/sched.h inc – 主要调度器 API
- man 2 getpriority,man 2 setpriority
- man 2 sched_setscheduler,man 2 sched_getscheduler
⚙️ 内部
- sched_init id 由 start_kernel id 调用。
- __schedule id 是主要的调度器函数。
- runqueues id,this_rq id
- kernel/schedsrc
- kernel/sched/core.csrc
- kernel/sched/fair.c src 实现 SCHED_NORMAL id,SCHED_BATCH id,SCHED_IDLE id
- sched_setscheduler id,sched_getscheduler id
- task_struct id::rt_priority id 和其他具有不太唯一标识符的成员
🛠️ 工具
📖 参考
- man 7 sched
- 调度 doc
- CFS
- 完全公平调度器 LWN
- 截止日期任务调度器 doc
- sched ltp
- sched_setparam ltp
- sched_getscheduler ltp
- sched_setscheduler ltp
📚 调度器相关进一步阅读
抢占是指系统中断正在运行的任务以切换到另一个任务的能力。这对于确保高优先级任务获得必要的 CPU 时间以及提高系统响应能力至关重要。在 Linux 中,抢占模型定义了内核如何以及何时可以抢占任务。不同的模型在系统响应能力和吞吐量之间提供了不同的权衡。
📖 参考
- kernel/Kconfig.preemptsrc
- CONFIG_PREEMPT_NONE id – 服务器禁用强制抢占
- CONFIG_PREEMPT_VOLUNTARY id – 桌面启用自愿抢占
- CONFIG_PREEMPT id – 除关键代码段外均可抢占,适用于低延迟桌面
- CONFIG_PREEMPT_RT id – 实时抢占,适用于高响应应用程序
- CONFIG_PREEMPT_DYNAMIC id,参见 /sys/kernel/debug/sched/preempt
内核中的等待队列是一种数据结构,它允许一个或多个进程等待(休眠),直到发生感兴趣的事情。它们在整个内核中被用于等待可用内存、I/O 完成、消息到达以及许多其他事情。在 Linux 的早期,等待队列只是一个简单的等待进程列表,但各种可扩展性问题(包括惊群效应)导致此后添加了相当多的复杂性。
⚲ API
wait_queue_head id 由wait_queue_entry id的双向链表和一个自旋锁组成。
等待简单事件
- 使用两种方法之一初始化wait_queue_head id
- init_waitqueue_head id 在函数上下文中初始化wait_queue_head id
- DECLARE_WAIT_QUEUE_HEAD id - 在全局上下文中实际定义wait_queue_head id
- 等待替代方案
- wait_event_interruptible id - 首选等待方式
- wait_event_interruptible_timeoutid
- wait_event id - 不可中断等待。可能导致死锁⚠
- wake_up id 等
👁 例如,请参见对唯一suspend_queue id的引用。
在复杂情况下显式使用 add_wait_queue 而不是简单的 wait_event
- DECLARE_WAITQUEUE id 实际上定义了带有default_wake_function id的 wait_queue_entry
- add_wait_queue id 将进程插入等待队列的首位
- remove_wait_queueid
⚙️ 内部
📖 参考
线程同步被定义为一种机制,它确保两个或多个并发进程或线程不会同时执行某个特定的程序段,该程序段称为互斥(互斥量)。当一个线程开始执行临界区(程序的序列化段)时,另一个线程应该等待,直到第一个线程完成。如果未应用正确的同步技术,则可能导致竞争条件,其中变量的值可能不可预测,并且根据进程或线程的上下文切换时间而有所不同。
一个man 2 futex ↪ do_futex id(“快速用户空间互斥量”的缩写)是一个内核系统调用,程序员可以使用它来实现基本锁定,或作为构建更高级别锁定抽象(如信号量和 POSIX 互斥量或条件变量)的构建块。
Futex 由一个内核空间等待队列组成,该队列附加到用户空间中的一个对齐整数。多个进程或线程完全在用户空间中操作该整数(使用原子操作以避免相互干扰),并且仅在请求等待队列上的操作时才诉诸相对昂贵的系统调用(例如,唤醒等待的进程,或将当前进程放入等待队列)。正确编程的基于 futex 的锁除了在锁被争用时之外不会使用系统调用;由于大多数操作不需要进程之间的仲裁,因此在大多数情况下不会发生这种情况。
Futex 的基本操作仅基于两个核心操作futex_wait id 和futex_wake id,尽管实现具有更多操作以用于更专业的情况。
- WAIT(addr,val)检查存储在地址addr处的值是否为val,如果是,则使当前线程休眠。
- WAKE(addr,val)唤醒在地址addr处等待的val个线程。
⚲ API
⚙️ 内部实现:kernel/futex.c src
📖 参考
⚲ API:man 2 flock
💾 历史:信号量是 System V IPC 的一部分 man 7 sysvipc
⚲ API
⚙️ 内部实现:ipc/sem.c src
对于内核模式同步,Linux 提供了三类锁定原语:睡眠锁、每个 CPU 本地锁和自旋锁。
解决读者-写者问题的常用机制是读-复制-更新 (RCU) 算法。读-复制-更新实现了一种对读者来说是无等待(非阻塞)的互斥,允许极低的开销。但是,RCU 更新可能代价很高,因为它们必须保留数据结构的旧版本以适应预先存在的读者。
💾 历史:RCU 于 2002 年 10 月添加到 Linux 中。从那时起,内核中出现了数千个 RCU API 的使用,包括网络协议栈和内存管理系统。Linux 内核 2.6 版本中 RCU 的实现是比较知名的 RCU 实现之一。
⚲ linux/rcupdate.h inc 中的核心 API 非常小
- rcu_read_lock id 标记 RCU 保护的数据结构,以便在整个关键段期间不会被回收。
- rcu_read_unlock id 用于读者通知回收器读者正在退出 RCU 读侧关键段。请注意,RCU 读侧关键段可以嵌套和/或重叠。
- synchronize_rcu id 会阻塞,直到所有 CPU 上所有预先存在的 RCU 读侧关键段都已完成。请注意,
synchronize_rcu
不会等待任何后续的 RCU 读侧关键段完成。
👁 例如,考虑以下事件序列
CPU 0 CPU 1 CPU 2 ----------------- ------------------------- --------------- 1. rcu_read_lock() 2. enters synchronize_rcu() 3. rcu_read_lock() 4. rcu_read_unlock() 5. exits synchronize_rcu() 6. rcu_read_unlock()
- 由于
synchronize_rcu
是必须确定读者何时完成的 API,因此它的实现是 RCU 的关键。为了使 RCU 在除最读密集型情况外的所有情况下都可用,synchronize_rcu
的开销也必须非常小。
- 或者,
synchronize_rcu
可以注册一个回调函数,而不是阻塞,该回调函数在所有正在进行的 RCU 读侧关键段完成后被调用。此回调变体在 Linux 内核中称为call_rcu id。
- rcu_assign_pointer id - 更新者使用此函数将新值分配给 RCU 保护的指针,以便安全地将值的变化从更新者传达给读者。此函数返回新值,并执行给定 CPU 架构所需的任何内存屏障 指令。也许更重要的是,它用于记录哪些指针受 RCU 保护。
- rcu_dereference id - 读者使用此函数获取 RCU 保护的指针,该指针返回一个可以安全取消引用的值。它还执行编译器或 CPU 所需的任何指令,例如,gcc 的易失性强制转换、C/C++11 的 memory_order_consume 加载或旧 DEC Alpha CPU 所需的内存屏障指令。
rcu_dereference
返回的值仅在包含的 RCU 读侧关键段内有效。与rcu_assign_pointer
一样,rcu_dereference
的一个重要功能是记录哪些指针受 RCU 保护。
RCU 基础设施观察 rcu_read_lock
、rcu_read_unlock
、synchronize_rcu
和 call_rcu
调用的时间序列,以确定 (1) synchronize_rcu
调用何时可以返回到其调用者,以及 (2) call_rcu
回调何时可以被调用。RCU 基础设施的高效实现大量使用批处理来分摊其在相应 API 的许多使用上的开销。
⚙️ 内部
📖 参考
⚲ API
- linux/mutex.hinc
- linux/completion.hinc
- mutex id 具有所有者和使用约束,比信号量更容易调试
- rt_mutex id 带有优先级继承 (PI) 支持的阻塞互斥锁
- ww_mutex id 缠绕/等待互斥量:带有死锁避免的阻塞互斥锁
- rw_semaphore id 读者-写者信号量
- percpu_rw_semaphoreid
- completion id - 使用 completion 用于中断服务程序和任务或两个任务之间的同步。
💾 历史
- semaphore id - 如果可能,请使用互斥量代替信号量
- linux/semaphore.hinc
- linux/rwsem.hinc
📖 参考
在普通的可抢占内核中,local_lock 调用preempt_disable id。在 RT 可抢占内核中,local_lock 调用migrate_disable id 和 spin_lock id。
⚲ API
📖 参考
💾 历史:在内核版本 2.6 之前,Linux 禁用中断以实现短关键段。从 2.6 及更高版本开始,Linux 是完全抢占式的。
自旋锁是一种锁,它会导致尝试获取它的线程简单地在循环中等待(“自旋”),同时重复检查锁是否可用。由于线程保持活动状态但没有执行有用的任务,因此使用此类锁是一种忙等待。一旦获取,自旋锁通常会一直保持,直到显式释放,尽管在某些实现中,如果等待的线程(持有锁的线程)阻塞或“进入睡眠”,它们可能会自动释放。
自旋锁通常在内核内部使用,因为如果线程可能仅被阻塞很短时间,它们效率很高。但是,如果持有自旋锁的时间过长,它们就会变得浪费,因为它们可能会阻止其他线程运行并需要重新调度。👁 例如,kobj_kset_join id 使用自旋锁来保护对链接列表的访问。
启用和禁用内核抢占替换了单处理器系统上的自旋锁(禁用CONFIG_SMP id)。大多数自旋锁在CONFIG_PREEMPT_RT id 内核中变成了睡眠锁。
📖 参考
seqlock(顺序锁的简称)是一种特殊的锁定机制,用于 Linux 支持在两个并行操作系统例程之间快速写入共享变量。当写者数量较少时,它是读者-写者问题的特殊解决方案。
这是一种读写一致性机制,避免了写者饥饿的问题。一个seqlock_t id除了锁之外,还包含用于保存序列计数器seqcount_t id/seqcount_spinlock_t 的存储空间。锁用于支持两个写者之间的同步,而计数器则用于指示读者的数据一致性。除了更新共享数据外,写者还会在获取锁之后和释放锁之前递增序列计数器。读者在读取共享数据之前和之后都会读取序列计数器。如果计数器在任一时刻为奇数,则表示在读取数据期间写者获取了锁,并且数据可能已更改。如果序列计数器不同,则表示在读取数据期间写者已更改数据。在这两种情况下,读者都会简单地重试(使用循环),直到他们在前后读取到相同的偶数序列计数器为止。
💾 历史:语义在 2.5.59 版本中稳定下来,并且存在于 2.6.x 稳定内核系列中。seqlocks 由 Stephen Hemminger 开发,最初称为 frlocks,基于 Andrea Arcangeli 的早期工作。第一个实现是在 x86-64 时间代码中,在那里需要与用户空间同步,而用户空间无法使用真正的锁。
⚲ API
👁 示例:mount_lock id,定义在fs/namespace.c src
📖 参考
在服务器上 在抢占式 RT 上 spinlock_t, raw_spinlock_t rt_mutex_base, rt_spin_lock, 睡眠 rwlock_t 自旋 睡眠 local_lock preempt_disable migrate_disable, rt_spin_lock, 睡眠
编译器可能会优化或重新排序对变量的写入,从而导致多个线程并发访问变量时出现意外行为。
⚲ API
- asm-generic/rwonce.h inc – 防止编译器合并或重新获取读取或写入操作。
- linux/compiler.hinc
- barrier id – 防止编译器重新排序屏障周围的指令
- asm-generic/barrier.h inc – 通用屏障定义
- arch/x86/include/asm/barrier.h src – 强制严格的 CPU 顺序
- mb id – 确保屏障之前的内存操作在屏障之后的任何内存操作开始之前完成
📚 进一步阅读
⚲ UAPI
- uapi/linux/time.hinc
- timespec id — 纳秒分辨率
- timeval id — 微秒分辨率
- 时区id
- ...
- uapi/linux/time_types.hinc
- __kernel_timespec id — 纳秒分辨率,用于系统调用
- ...
⚲ API
- linux/time.hinc
- tmid
- get_timespec64id
- ...
- linux/ktime.hinc
- ktime_t id — 内核时间值的纳秒标量表示
- ktime_subid
- ...
- linux/timekeeping.hinc
- linux/time64.hinc
- uapi/linux/rtc.hinc
- linux/jiffies.hinc
⚙️ 内部
📖 参考
⚙️ 锁定内部
📚 锁定参考
- 锁定 doc
- 锁类型及其规则 doc
- 睡眠锁 doc
- mutex id, rt_mutex id, semaphore id, rw_semaphore id, ww_mutex id, percpu_rw_semaphore id
- 在抢占式 RT 上:local_lock, spinlock_t, rwlock_t
- 自旋锁 doc:
- raw_spinlock_t, 位自旋锁
- 在非抢占式 RT 上:spinlock_t, rwlock_t
- 睡眠锁 doc
- 锁类型及其规则 doc
- 不可靠的锁定指南 doc
- 同步(计算机科学)
- 同步原语
- 无tick内核 (全dynticks), CONFIG_NO_HZ_FULL id
一个中断 是由硬件或软件发出的处理器信号,指示需要立即关注的事件。中断提醒处理器注意需要中断处理器当前正在执行的代码的高优先级条件。处理器通过暂停其当前活动、保存其状态并执行称为中断处理程序(或中断服务例程,ISR)的函数来处理该事件来响应。此中断是临时的,并且在中断处理程序完成后,处理器恢复正常活动。
中断有两种类型:硬件中断和软件中断。硬件中断由设备用于通信它们需要来自操作系统的关注。例如,按下键盘上的按键或移动鼠标会触发硬件中断,导致处理器读取按键或鼠标位置。与软件类型不同,硬件中断是异步的,并且可能发生在指令执行的中间,这需要在编程中格外小心。发起硬件中断的动作称为中断请求 - IRQ (⚙️ do_IRQ id).
软件中断是由处理器本身的异常情况或指令集中导致执行时中断的特殊指令引起的。前者通常称为陷阱 (⚙️ do_trap id) 或异常,用于程序执行期间发生的错误或事件,这些错误或事件异常到足以无法在程序本身中处理。例如,如果处理器的算术逻辑单元被命令将一个数字除以零,则此不可能的要求将导致除以零异常 (⚙️ X86_TRAP_DE id),可能会导致计算机放弃计算或显示错误消息。软件中断指令的功能类似于子程序调用,并用于各种目的,例如请求来自低级系统软件(如设备驱动程序)的服务。例如,计算机通常使用软件中断指令与磁盘控制器通信,以请求读取或写入磁盘的数据。
每个中断都有自己的中断处理程序。硬件中断的数量受处理器中断请求 (IRQ) 线路数量的限制,但可能有数百个不同的软件中断。
⚲ API
- /proc/interrupts
- man 1 irqtop – 用于显示内核中断信息的实用程序
- irqbalance – 在多处理器系统上的处理器之间分配硬件中断
- 有许多方法可以请求 ISR,其中两种是
- devm_request_threaded_irq id – 为具有线程化 ISR 的受管理设备分配中断线的首选函数
- request_irq id, free_irq id – 添加和删除中断线处理程序的旧且常用的函数
- linux/interrupt.h inc – 主要中断支持头文件
- irqaction id – 包含处理程序函数
- linux/irq.hinc
- include/linux/irqflags.hinc
- linux/irqdesc.hinc
- linux/irqdomain.hinc
- irq_domain id – 硬件中断号转换对象
- irq_domain_get_irq_dataid
- linux/msi.h inc – 消息信号中断
- 结构体的结构
- irq_desc id 是…的容器
⚙️ 内部
- kernel/irq/settings.hsrc
- kernel/irqsrc
- ls /sys/kernel/debug/irq/domains/
- irq_chipid
📖 参考
👁 示例
- dummy_irq_chip id – 虚拟中断芯片实现
- lib/locking-selftest.csrc
⚲ API
- /proc/irq/default_smp_affinity
- /proc/irq/*/smp_affinity 和 /proc/irq/*/smp_affinity_list
常用类型和函数
- struct irq_affinity id – 自动 irq 亲和性分配的描述,参见 devm_platform_get_irqs_affinity id
- struct irq_affinity_desc id – 中断亲和性描述符,参见 irq_update_affinity_desc id, irq_create_affinity_masks id
- irq_set_affinityid
- irq_get_affinity_maskid
- irq_can_set_affinityid
- irq_set_affinity_hintid
- irqd_affinity_is_managedid
- irq_data_get_affinity_maskid
- irq_data_get_effective_affinity_maskid
- irq_data_update_effective_affinityid
- irq_set_affinity_notifierid
- irq_affinity_notifyid
- irq_chip_set_affinity_parentid
- irq_set_vcpu_affinityid
🛠️ 工具
- irqbalance – 将硬件中断分配到 CPU 上
📖 参考
📚 进一步阅读
- IDT – 中断描述符表
⚲ API
devm_request_threaded_irq id, request_threaded_irq id
ISR 应返回 IRQ_WAKE_THREAD 以运行线程函数
⚙️ 内部
📖 参考
work 是 workqueue 的包装器
⚲ API
- linux/workqueue.hinc
- work_struct id, INIT_WORK id, schedule_work id,
- 延迟工作相关的函数:delayed_work id,INIT_DELAYED_WORK id,schedule_delayed_work id,cancel_delayed_work_sync id
👁 使用示例 samples/ftrace/sample-trace-array.c src
⚙️ 内部实现:system_wq id
⚲ API
⚙️ 内部
📖 参考
- linux/irq_work.h inc – 用于从硬中断上下文中排队和运行回调的框架
此定时器是用于具有jiffies分辨率的周期性任务的软中断。
⚲ API
- linux/timer.hinc
- 定时器相关的结构体和函数:timer_list id,DEFINE_TIMER id,timer_setup id
- mod_timer id — 设置以jiffies为单位的超时时间。
- del_timerid
⚙️ 内部
👁 示例
⚲ API
- linux/hrtimer.hinc
- hrtimer id,hrtimer.function — 回调函数
- hrtimer_init id,hrtimer_cancel id
- hrtimer_start id 以纳秒分辨率启动定时器
👁 示例 watchdog_enable id
⚙️ 内部
📚 高分辨率定时器相关参考
📚 定时器相关参考
tasklet 是一种软中断,用于时间关键操作。
⚲ API 已被弃用,建议使用线程化中断:devm_request_threaded_irq id
- tasklet 相关的结构体和函数:tasklet_struct id,tasklet_init id,tasklet_schedule id
⚙️ 内部实现:tasklet_action_common id HI_SOFTIRQ,TASKLET_SOFTIRQ
软中断是内部系统机制,不应直接使用。请使用 tasklet 或线程化中断。
⚲ API
- cat /proc/softirqs
- open_softirq id 注册 softirq_action id
⚙️ 内部
⚲ API
📖 参考
🖱️ 图形界面
- tuna – 用于调整正在运行的进程的程序
⚲ API
- cat /proc/cpuinfo
- /sys/devices/system/cpu/
- /sys/cpu/
- /sys/fs/cgroup/cpu/
- grep -i cpu /proc/self/status
- rdmsr – 用于读取 CPU 机器特定寄存器 (MSR) 的工具
- man 1 lscpu – 显示有关 CPU 架构的信息
- linux/arch_topology.h inc – 体系结构特定的 CPU 拓扑信息
- linux/cpu.h inc – 通用 CPU 定义
- linux/cpu_cooling.hinc
- linux/cpu_pm.hinc
- linux/cpufeature.hinc
- linux/cpufreq.hinc
- linux/cpuhotplug.h inc – CPU 热插拔状态
- linux/cpuidle.h inc – CPU 空闲电源管理的通用框架
- linux/peci-cpu.hinc
- linux/sched/cpufreq.h inc – cpufreq 驱动程序和调度程序之间的接口
- linux/sched/cputime.h inc – cputime 计量 API
⚙️ 内部
⚙️ 内部
📖 参考
本章介绍Linux内核的多处理和多核方面。
Linux SMP的关键概念和特性包括
- 对称性:在SMP系统中,所有处理器都被认为是相同的,没有硬件层次结构,这与使用协处理器形成对比。
- 负载均衡:Linux内核采用负载均衡机制,将任务均匀地分配到可用的CPU内核上。这可以防止任何一个内核过载,而其他内核则处于未充分利用状态。
- 并行性:SMP支持并行处理,多个线程或进程可以在不同的CPU内核上同时执行。这可以显著提高为利用多个线程而设计的应用程序的执行速度。
- 线程调度:Linux内核调度程序负责确定哪些线程或进程在哪些CPU内核上运行以及运行多长时间。它旨在通过最大限度地减少争用和最大限度地提高CPU利用率来优化性能。
- 共享内存:在SMP系统中,所有CPU内核通常共享相同的物理内存空间。这使得在不同内核上运行的进程和线程能够更有效地通信和共享数据。
- NUMA – 非一致内存访问:在较大的SMP系统中,由于内存库和处理器的物理排列,内存访问时间可能不一致。Linux具有有效处理NUMA架构的机制,允许将进程调度到更靠近其关联内存的CPU上。
- 缓存一致性:SMP系统需要机制来确保所有CPU内核对内存都有一致的视图。缓存一致性协议确保对共享内存位置所做的更改正确传播到所有内核。
- 可扩展性:SMP系统可以扩展到包含更多CPU内核,从而增强系统的整体计算能力。但是,随着内核数量的增加,可能会出现与内存访问、争用和内核之间通信相关的挑战。
- 内核和用户空间:在用户空间中运行的Linux应用程序可以利用SMP,而无需了解底层硬件细节。内核负责管理CPU内核和资源分配。
🗝️ 关键术语
- 亲缘性是指将进程或线程分配到特定的CPU内核。这有助于控制哪些CPU执行任务,并可能通过减少内核之间的数据移动来提高性能。它可以使用系统调用或命令进行管理。亲缘性可以表示为CPU位掩码:cpumask_t id或CPU亲缘性列表:cpulist_parse id。
⚲ API
ps -PLe
– 列出线程及其最后执行的处理器(第三列PSR)。- man 1 taskset – 设置或检索进程的CPU亲缘性
- man 2 getcpu – 确定调用线程正在运行的CPU和NUMA节点
- man 7 cpuset – 将进程限制在处理器和内存节点子集
- man 8 chcpu – 配置CPU
- man 3 CPU_SET – 用于操作CPU集的宏
- grep Cpus_allowed /proc/self/status
- man 2 sched_setaffinity man 2 sched_getaffinity – 设置和获取线程的CPU亲缘性掩码
- set_cpus_allowed_ptr id – 更改任务亲缘性掩码的常用内核函数
- linux/smp.hinc
- linux/cpu.hinc
- linux/group_cpus.h inc: group_cpus_evenly id – 根据NUMA/CPU位置均匀地对所有CPU进行分组
- linux/cpuset.h inc – cpuset接口
- linux/cpu_rmap.h inc – CPU亲缘性反向映射支持
- linux/cpumask_types.hinc
- struct cpumask, cpumask_t id – CPU位图,可能非常大
- cpumask_var_t id – 用于本地cpumask变量的类型,参见alloc_cpumask_var id,free_cpumask_var id。
- linux/cpumask.h inc – Cpumasks提供了一个位图,适合表示系统中的CPU集,每个CPU编号一个位位置
- asm-generic/percpu.hinc
- linux/percpu-defs.h inc – percpu区域的基本定义
- linux/percpu.hinc
- linux/percpu-refcount.hinc
- linux/percpu-rwsem.hinc
- linux/preempt.hinc
- /sys/bus/cpu
- 每个CPU的local_lock
⚙️ 内部
- boot_cpu_init id激活第一个CPU
- smp_prepare_cpus id在启动期间初始化其余CPU
- cpuset_initid
- cpu_numberid
- cpus_mask id – task_struct id的亲缘性
- CONFIG_SMPid
- trace/events/percpu.hinc
- IPI – 处理器间中断
- smpboot
- lib/group_cpus.csrc
🛠️ 工具
- irqbalance – 将硬件中断分配到 CPU 上
- man 8 numactl – 控制进程或共享内存的NUMA策略
📖 参考
- cgroup v1的CPUSETS doc
- 命令行参数中的CPU列表 doc
- nohz_full 清除管家工作。cpumasks id 用于tick、wq、timer、rcu和kthread在housekeeping_nohz_full_setup id中。
- isolcpus 清除管家工作。cpumasks id 用于tick、domain和managed_irq在housekeeping_isolcpus_setup id中。
📚 进一步阅读
- CPU隔离技术现状,LPC'23
- CPU分区
- 调度程序域 doc – 调度程序在调度程序域内平衡CPU(调度组)
- CPU隔离
- LKML上的isolcpus
- LKML上的nohz_full
- 调度程序TuneD插件的功能
⚲ API
- linux/cpuhotplug.h inc – CPU 热插拔状态
⚙️ 内部
📖 参考
📚 进一步阅读
内存屏障 (MB) 是用于确保 SMP 环境中内存操作正确排序的同步机制。它们在维护不同 CPU 内核或处理器之间共享数据的完整性和正确性方面发挥着至关重要的作用。MB 可以防止编译器或 CPU 对内存访问指令进行意外且可能造成危害的重新排序,这可能导致并发软件系统中的数据损坏和竞争条件。
⚲ API
⚙️ 内部
📖 参考
Linux CPU 体系结构指的是与 Linux 操作系统兼容的不同类型的中央处理器 (CPU)。Linux 旨在运行在各种 CPU 体系结构上,这使其能够在各种设备上使用,从智能手机到服务器和超级计算机。每个体系结构都有其自身独特的特性、优势和设计考虑因素。
体系结构按系列(例如 x86、ARM)、字 或长整型 大小(例如 CONFIG_32BIT id、CONFIG_64BIT id)进行分类。
一些针对不同 CPU 体系结构具有不同实现的函数
- do_boot_cpu id > start_secondary id > cpu_init id
- setup_arch id, start_thread id, get_current id, current id
⚲ API
⚙️ 架构内部
📖 参考
📚 关于多任务处理、调度和 CPU 的进一步阅读
- ↑ a b Malte Skarupke. "测量互斥锁、自旋锁以及 Linux 调度程序到底有多糟糕".