linux内核设计与实现

2023-05-16

一. linux内核简介

1. linux简介

1.1 unix的特点

unix很简洁,仅提供几百个系统调用,并有非常明确的设计目的

unix所有东西都当作文件对待,这种抽象使对数据和设备都通过一套相同的系统调用接口进行

内核用C语言编写,移植能力很强

进程创建迅速,独特的fork调用

提供了简洁但是稳定的进程间通讯原语

1.2 unix和linux

linux克隆unix,但不是unix

linux借鉴了unix很多的设计,并且实现了 unix的api

linux没有直接使用unix的源代码,但完整表达了unix的设计目标并保证编程接口一致

2. 操作系统和内核简介

内核一般包括:

中断服务程序:负责响应中断

调度程序:管理多进程,分配处理器时间

内存管理程序:管理内存空间

系统服务程序:包括网络,进程间通讯

应用程序通过系统调用和内核通讯来运行

应用程序通常调用库函数,库函数通过系统调用让内核带其完成各种任务

内核对硬件设备的管理:硬件想要通讯时,发送异步信号去打断内核,内核通过中断号查找处理程序

linux内核开发的特定

不能链接标准c函数库。c库太大了,会影响大小和效率。不过大部分常用的c函数在内核中都有实现

没有内存保护机制,要注意非法访问内存地址

不要轻易使用浮点数,要人工保存和恢复浮点寄存器

栈空间很小且固定。32为机器为8kb,64为16kb

内核很容易产生竞争条件,注意同步和并发

注意可移植性

二. 进程管理

1. 基本概念

unix系统的两大抽象对象:进程,文件。

进程是处于执行期的程序,linux通常也把进程叫做任务

进程包括:代码段,数据段,打开的文件,挂起的信号,地址空间,线程等

线程是进程中活动的执行对象

每个线程拥有独立的程序计数器,进程栈和一组进程寄存器

内核调度的对象是线程,而不是进程

linux的线程实现非常特别,并不特别区分线程和进程

进程提供两种虚拟机制:虚拟处理器和虚拟内存

同一个进程内的线程可以共享虚拟内存,但是有各自的虚拟处理器

2. 进程描述符及任务队列

2.1 基本概念

内核把进程存放在叫做任务队列的双向循环链表中

链表中每一项都是task_struct类型,称为进程描述符,包括一个进程的所有信息。路径:/include/linux/sched.h

2.2 进程描述符如何分配

linux通过slab分配其分配task_struct结构,这样能达到对象复用和缓存着色

通过预先分配和重复使用task_struct,避免动态分配和释放带来的性能损耗,这也是为什么创建进程快的原因

task_struct放在内核栈的尾端,为了让寄存器少的硬件体系只通过栈指针就能算出位置,避免使用额外寄存器存储

slab分配器在内核栈的尾部创建新的struct thread_info,内部的task指向实际的task_struct。thread_info位置:<asm/thread_info.h>

2.3 进程描述符存放在哪

current宏可以查找当前正在运行进程的进程描述符

这个宏的具体实现根据各自硬件体系结构有所不同

x86体系,通过栈尾部的thread_info结构的task指针找到进程描述符

有的体系(IBM的RISC),分配一个专用的寄存器存放task_struct的地址

2.4 进程的状态

进程描述符的state字段描述了进程当前的状态,每个进程都处于五种状态的一种

TASK_RUNNING:运行。进程是可执行的。

TASK_INTERRUPTIBLE:可中断。进程被阻塞,等待被唤醒

TASK_UNINTERRUPTIBLE:不可中断。收到信号不做任何响应。ps命令查看会显示D

TASK_ZOMBIE:僵死。进程已经结束了,但是父进程还没有调用wait4系统调用

TASK_STOPPED:停止。进程停止执行

状态变迁图

设置当前进程:set_task_state(task,state)或set_current_state

2.5 进程上下文

一般程序在用户空间执行,执行系统调用或触发异常时,进入内核空间,内核代表进程执行,并处于进程上下文中

系统调用和异常处理是内核明确定义的接口,对内核的所有访问也只能通过这些接口

linux进程有明显的继承关系,所有的进程都是pid为1的init进程的后代

系统中的每个进程必有一个父进程,每个进程可以拥有一个或多个子进程

进程间关系存放在进程描述符中。task_struct中的parent变量,指向一个task_struct,存放父进程地址。children变量是一个链表,指向所有的子进程

3. 进程创建

3.1 基本概念

unix进程创建分为:fork和exec两步

fork通过拷贝当前进程创建子进程。子进程和父进程仅有很少差异:pid,ppid,某些资源和统计量

exec负责读取可执行文件并载入地址空间开始运行

3.2 写时拷贝(COW)

传统的fork直接拷贝资源效率低下,linux使用写时拷贝(copy on write)技术提高效率

COW并不会复制整个地址空间,而是让父子进程以只读方式共享内存,数据的复制只有在写入时才进行

3.3 fork函数

linux通过clone()系统调用实现fork(), 这个调用通过参数标识(很多种类型)指明需要共享的资源

clone内部调用do_fork完成主要工作(kernel/fork.c)

do_fork内部调用copy_process,然后让进程运行

copy_process调用过程:

调用dup_task_struct为新进程创建内核栈,thread_info结构和task_struct,这些值与当前进程相同,此时描述符完全相同

检查系统拥有的进程数是否超过限制

将很多成员重置

设置状态为TASK_UNINTERRUPTIBLE保证不会被运行

调用copy_flags以更新task_struct的flags成员

调用get_pid获取新的pid

根据参数标识,拷贝或共享打开的文件,文件系统信息,信号处理函数,进程地址空间,命名空间等。一般情况下,这些资源是线程共享的

父子进程平分时间片

扫尾工作,并返回指向子进程的指针

新创建的进程被唤醒并让其投入运行,一般优先子进程首先执行

3.4 vfork函数

和fork功能相同,除了补考吧父进程的页表项

通过向clone系统调用传递一个特殊标志进行的

该函数的设计并不是很优良的

【文章福利】小编推荐自己的Linux内核技术交流群: 【977878001】整理一些个人觉得比较好得学习书籍、视频资料;进群私聊群管理领取 内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

4. 线程在linux中的实现

4.1 liunx线程概述

一组线程共享进程内的内存地址空间,打开的文件和其他资源

线程机制支持并发程序设计技术,多处理器上保证真正的并行处理

linux实现线程的机制非常独特,从内核角度看,没有线程的概念

linux把所有线程都当做进程来实现,内核没有特别的调度算法或数据结构来表征线程,被视为一个使用某些共享资源的进程

每个线程有自己的task_struct,就像一个普通的进程,这个进程和其他进程共享某些资源

与其他系统(windows,solaris)实现差异巨大,这些系统内核专门提供线程的支持

4.2 linux线程创建

线程的创建和普通进程创建类型,只不过调用clone时需要传递一些参数标志,指明需要共享的资源

参数标志说明:

CLONE_VM:父子进程共享地址空间

CLONE_SIGHAND:父子进程共享信号处理函数

CLONE_THREAD:父子进程放入相同线程组

CLONE_FS:父子进程共享文件系统信息

CLONE_FILES:共享打开的文件

...

4.3 内核线程

内核线程:独立运行在内核空间的标准进程

和普通进程的区别:没有独立的地址空间,只能在内核空间运行

创建只能由其他内核线程创建,函数为kernel_thread

4.4 进程终结

释放资源

进程终结时,内核必须释放它所占有的资源,并通知父进程

结束可以是正常,异常,还可以注册终止清理函数

最终结束会调用do_exit(kenel/exit.c),完成的工作包括:

将task_struct的标志成员设置为PF_EXITING

如果进程会计功能开启,会调用acct_process输出统计信息

调用_exit_mm函数放弃进程占用的mm_struct,如果没有被共享就彻底释放

调用sem_exit。如果排队等待IPC信号,则离开队列

调用__exit_files:递减文件描述符;__exit_fs:递减文件系统数据;exit_namespace:名字空间引用计数;exit_sighand:信号处理函数引用计数,如果某个降为0,则可释放

task_struct的exit_code设置退出代码

调用exit_notify向进程发送信号,父进程修改为其他线程或init进程,进程状态设置为TASK_ZOMBLE(僵死,不被调度)

最后,调用schedule切换到其他进程

调用完do_exit,与进程相关的所有资源都被释放了,它所占有的资源只剩报错thread_info的内核栈和保存task_struct的那一小片slab,存在的唯一目的就是向父进程提供信息。

删除进程描述符

调用do_exit之后,线程僵死,但是还保留文件描述符

父进程获取到子进程的信息后,子进程的task_sturct结构才被释放

wait函数调用系统函数wait4实现,将挂起调用它的进程,直到其中一个子进程退出,函数返回子进程的pid

当最终需要释放进程描述符时,release_task会被调用,执行以下工作:

调用free_uid减少该进程拥有者的进程使用计数

调用unhash_process从pidhash上删除该进程,同时从task_list删除该进程

如果进程正在被ptrace跟踪,将跟踪父进程重置

最后,调用put_task_struct释放内核栈和thread_info结构所占的页,并释放task_struct所占的slab高速缓存

这时资源和描述符就全部被释放掉了

孤儿进程的处理

父进程如果在子进程之前退出,必须找到新的父亲,否则永远僵死

寻找父亲的函数在do_exit中调用的notify_present函数,内部调用forget_original_parent,该函数实现具体寻找过程

该函数设置父亲为线程组内的其他进程,没有就用init进程

三. 进程调度

1. 概述

调度程序是内核组成部分,它负责选择下一个要运行的进程

调度程序负责给可运行进程分配处理器时间资源

多任务系统可分为:抢占式任务(linux等现代操作系统的方式)和非抢占式任务

分配给每个进程的执行时间叫做时间片

2. 调度策略

2.1 cpu密集型和IO密集型

cpu密集型:大部分时间执行代码

IO密集型:大部分时间提交io和等待io,经常可运行,但运行时间极短

从系统响应速度考虑,linux调度策略更倾向于优先调度IO密集型进程

2.2 进程优先级

调度算法中最基本的一类:基于优先级调度,根据进程的价值和其对处理器时间的需求分级的思想

调度程序总是选择时间片未用完且优先级最高的进程运行

linux实现了一种基于动态优先级的调度算法。一开始设置基本优先级,然后根据需要动态加,减优先级:如果一个进程IO等待时间多余运行时间,它属于IO密集型,会提高优先级;相反,如果进程时间片一下就别耗尽,属于cpu密集型,会降低优先级

linux提供两组独立的优先级范围:

nice值:-20~19,默认为0。标准优先级范围。值越大,优先级越低,时间片越短。task_struct的static_prio字段表示

实时优先级:0~99

2.3 时间片

表明进程在被抢占之前能持续运行的时间

调度策略必须规定默认时间片。如果过长,交互式的响应表现欠佳;如果过短,会明显增大进程切换带来的处理器耗时

很多系统默认时间片很短:20ms

linux提供动态调整优先级和时间片长度的机制,使得调度性能稳定且强健

进程不一定要一次用完时间片,可分多次使用,尽可能长时间保证可运行

2.4 进程抢占

当一个进程处于TASK_RUNNING状态,内核会检查它的优先级是否高于正在运行的进程,满足的话调度程序被唤醒重新选择进程运行

当一个进程的时间片为0,它会被抢占,调度程序可以选择新的进程执行

3. 调度算法

3.1 概述

linux调度程序定义与kernel/sched.c

2.5版本内核重写调度算法,和以前版本区别很大,实现以下目标

充分实现O(1)调度,不管多少进程或什么输入,每个算法能在恒定时间内完成

每个处理器拥有自己的锁和自己的可执行队列

尽量将同一组任务分配给同一个cpu连续执行,减少在cpu间移动进程

加强交互性能,即使系统负载,也保证系统响应

保证公平。消除饥饿线程,减少大量时间片进程

3.2 可执行队列

可执行队列数据结构为kernel/sched.c文件下的runqueue

表示给定处理器上的可执行进程链表,每个处理器一个

可执行队列是调度程序核心的数据结构,提供很多宏获取该指针

cpu_rq(processor):返回给定处理器可执行队列指针

this_rq:当前处理器可执行队列指针

task_rq:给定任务所在队列指针

对队列进行操作时,需要锁住队列,加锁函数

task_rq_lock

task_rq_unlock

this_rq_lock

this_rq_unlock

double_rq_lock

double_rq_unlock

3.3 优先级数组

每个可执行队列都有两个优先级数组,一个活跃的和一个过期的

数据结构为kernel/sched.c文件下的prio_array

能提供O(1)级算法复杂度的数据结构

优先级数组使可运行处理器的每一种优先级都包含一个相应的队列,该队列包含该优先级上可执行进程链表

优先级数组还拥有一个优先级位图,帮助高效查询最高优先级可执行进程

MAX_PRIO:系统拥有的优先级个数,默认140

BITMAP_SISE:优先级位图数组大小,unsigned long为32位,要表示140个优先级,需要5个长整形,总共160位

每个优先级数组都要包含一个位图成员。开始时,所有位图为0,当某个优先级进程开始执行,相应位图变为1,查找最高优先级就变为查找位图为1的第一个值,查找时间恒定。函数为sched_find_first_bit

要查找给定优先级任务,并轮询时,只需要遍历某个优先级链表即可

nr_active表示优先级数组内可执行进程数目

3.4 重新计算时间片

当所有线程时间片用完时,老版本linux采用循环遍历计算的方式重新计算时间片

新版本调度程序通过维护两个优先级数组:活动数组和过期数组,过期时间耗尽的线程放入过期数组,异步计算过期数组的时间片。最后只需要交换两个数组即可

3.5 计算优先级和时间片

动态计算优先级

静态优先级由用户指定后就不可更改,即前面介绍的nice值

动态优先级根据静态优先级和进程交互性函数关系计算而来

effective_prio函数返回一个进程的动态优先级:该函数以nice值为基数,再根据交互程度加上奖惩值(-5~5)

交换程度判断:计算进程执行时间和休眠时间。task_struct的sleep_avg变量,默认为10ms,休眠时增加该值(休眠时长),运行时减少该值

动态计算时间片

进程创建时,父子进程平分时间片。防止创建多个进程以抢占更多cpu时间

任务时间片用完时,根据任务的动态优先级计算时间片。函数为task_timeslice。时间值根据优先级值按比例缩放。最高200ms,最低10ms,默认为100ms

如果一个交互性很强的进程(TASK_INTER_ACTIVE宏),时间片用完后,会被再次放入活动数组而不是过期数组。避免出现需要交互却因为没有等到两个数组交换而执行不了。实现代码:scheduler_tick函数

3.6 睡眠和唤醒

处于休眠的线程进入等待队列,里面保存所有因等待某些事件发生的进程组成的简单链表

等待队列的数据结构为wake_queue_head_t

等待队列的创建:DECLEAR_WAITQUEUE或init_waitqueue_head

加入等待队列:add_wait_queue

与等待队列相关的事件发生时,队列的进程会被唤醒,使用wake_up函数

3.7 负载平衡程序

负载平衡程序针对多处理器系统中可执行程序负载不均衡的情况

函数为kernel/sched.c中的load_balance函数

调用时机:可执行队列为空时,定时调用(系统空闲时每隔1ms调用,其他情况每隔200ms调用)。单处理器不需要此函数。

负载平衡调用时需要锁定当前队列,并且屏蔽中断

load_balance操作步骤:

调用find_busiest_queue,找到最繁忙的队列。该队列进程数最多。如果没有超过当前队列25%的队列,直接结束返回

从繁忙队列中选择一个优先级数组用来抽取进程,最好是过期数组

寻址含有优先级最高(值最小)的链表,把高优先级的进程分散开

找到链表中没有在执行,且可移动,且不在高速缓存中的进程,靠用pull_task将进程抽取到当前队列

只要队列不平衡就执行以上步骤。最后释放锁。

4. 抢占和上下文切换

4.1 概述

上下文切换是从一个进程切换到另一个进程。

上下文切换定义在kernel/sched.c中的context_switch函数,该函数完成两项基本工作

调用定义在include/asm/menu_context.h中的switch_mm,负责把虚拟内存从上一个进程映射切换到新进程中

调用定义在include/asm/system.h中的switch_to,负责从上一个进程的处理器状态切换到新进程的处理器状态。包括保存,恢复栈信息和寄存器信息

内核提供need_resched标志表明是否需要重新调度。某个进程用尽时间片时,schedular_tick会设置该标志;当一个高优先级进程进入可执行状态时,try_to_wake_up也会设置该标志

每个进程都包含need_resched标志

4.2 用户抢占

用户抢占:内核返回用户空间时,如果need_reshed被设置,导致调用schedule,会选择一个更合适的进程执行的情况

用户抢占在以下情况时发生:

从系统调用返回用户空间

从中断处理程序返回用户空间

4.3 内核抢占

大部分Unix其他变体和大部分操作系统,不支持内核抢占,内核代码需要一直执行直到完成

2.6版本内核中,添加了内核抢占。只要重新调度是安全的,就可以内核抢占

只要没有持有锁,重新调度就是安全的,可以内核抢占

持有锁使用thread_info中的preempt_count计数器表示,使用锁时数值加一,释放锁时数值减一

内核抢占在以下情况时发生:

从中断处理程序返回内核空间时

当内核代码再一次具有可抢占性时

内核中的任务显示调用schedule

内核中的任务阻塞

5. 与调度相关的系统调用

sched_setscheduler:设置task_struct的policy和rt_priority值

sched_setaffinity:设置task_struct的cpus_allowed这个位掩码标志

sched_yield:将进程从活动队列移动到过期队列,以让出执行时间

四. 系统调用

1. 概述

系统调用提供内核和应用程序交互的接口

系统调用内部,函数声明中要添加asmlinkage,通知编译期仅从栈中提取函数参数

系统调用在内核中均以sys_作为前缀

linux中每个系统调用都和一个独一无二的系统调用号关联

内核记录系统调用表所有已注册过的系统调用列表,存储在sys_call——table中,以体系结构有关

linux内核设计优化简洁,上下文切换时间极快,操作系统执行效率高

2. 系统调用处理程序

用户程序不能直接调用内核函数,以防止内核空间安全失控。而是通过中断通知内核,让内核代表程序去执行

触发软中断前,将调用号装入eax寄存器

参数传递:在x86系统上,ebx、ecx、edx、esi、edi按照顺序存放前五个参数。返回值通过eax寄存器返回

内核空间和用户空间数据拷贝函数:copy_to_user,copy_from_user

3. 系统调用上下文

current指针指向引发当前调用的进程

执行系统调用时处于进程上下文

进程上下文中,内核可以休眠(调用阻塞或schedule)并可以被抢占

4. 系统调用的实现

linux不提倡多用途的系统调用,每个系统调用都应该有明确的用途

接口应该尽量简洁,参数少。力求稳定不做改动

尽量为将来做考虑,尽量通用,不要做限制。“提供机制而不是策略”

编写完后要注册到内核,成为真正可用的系统调用

在系统调用最好加入一个表项。大部分位于entry.s文件中。所有支持系统调用的硬件体系都要做

定义系统调用号到include/asm/unist.h文件中

函数放入kernel文件下某个位置,使之编译进内核映像(不能被编译称模块)

用户空间如何访问注册的系统调用

通常情况下,用户通过包含标准头文件,并和底层系统调用具体的c实现链接,就可以使用系统调用

自定义系统调用在标志头文件中不存在,可以通过linux提供的宏来调用:_syscalln,n代表需要传递的参数。该宏有2+2n个参数,第一个代表返回值类型,第二个代表函数名称,后续的是n个参数类型和参数名称

比如:open函数的系统调用,系统调用号为_NR_open,定义在<asm/unistd.h>中,内部被_syscall3宏实现,调用open时,内部把宏直接放入应用程序代码中

五. 中断和中断处理程序

1. 中断

中断用于解决计算机处理器与硬件设备处理速度不匹配的问题,硬件处理好任务后主动发送信号给处理器

中断本质是电信号,由硬件设备产生,送入中断处理器的输入引脚上。再由中断控制器向处理器发送信号。处理器收到信号就中断工作去处理中断

每个中断都有唯一都数字标识,称为中断请求(IRQ)线。比如:IRQ0是时钟中断,IRQ1是键盘中断

2. 中断处理程序

响应特定中断时,会执行的函数为中断处理程序或中断服务例程

中断处理程序是设备驱动程序的一部分,设备驱动程序是用于对设备进行管理的内核代码

与内核函数的区别:中断处理程序是被内核调用来响应中断的,运行与中断上下文中

中断处理程序必须能快速执行,同时,我们要靠它完成大量的其他工作。这两个目的是矛盾的。

为了解决上述矛盾,将中断处理程序切分为两个半部

上半部:接收到请求就立即执行,但执行少部分工作。中断应答和硬件复位

下半部:中断处理程序返回时立即执行

3. 注册中断处理程序

irq:要分配的中断号

handler:实际中断处理程序。接受三个参数,返回irqreturn_t类型参数

irqflags:可以为0,或以下标识的位掩码

SA_INTERRUPT:表明是快速中断程序。主要用于时钟中断程序

SA_SAMPLE_RANDOM

SA_SHIRQ:共享中断线

devname:中断相关设备的Ascii文本名称,如键盘中段为“keyboard”

dev_id:用于共享中断线

该函数可能会休眠,不能在中断上下文或不允许阻塞的代码中调用

4. 中断处理机制

设备产生中断,把电信号发送给中断控制器

中断控制器把信号发给处理器

处理器中断程序,跳到内存中预定义的位置执行。就是中断程序入口点

内核调用do_IRQ,对中断进行应答

相关函数位于arch/i386/kernel/entry.s,arch/i386/kernel/irq.c

5 中断控制

linux提供一组接口用于操作机器上的中断状态,提供能够禁止中断系统或屏蔽中断线的功能

相关代码在<asm/system.h>, <asm/irq.h>中

中断控制的根源是提供同步,通过禁止中断,确保中断程序不会抢占当前代码,也可以禁止内核抢占

禁止和激活当前处理器中断:local_irq_disable,local_irq_enable

禁止(屏蔽)指定中断线: disable_irq,disable_irq_nosync,enable_irq,synchronize_irq

获取中断系统状态:asm/system.h中的irqs_disable

判断是否处于中断上下文中:asm/hardirq.h中的in_interrupt

六. 内核同步

1. 基本概念

临界区:访问和操作共享数据的代码段

竞争条件:多个执行线程处于同一个临界区

同步:避免并发和防止竞争条件

为什么需要同步:用户程序会被调度程序抢占和重新调度

造成并发的原因有:

中断

内核抢占

睡眠及用户空间的同步

多处理器

同步问题的解决:加锁

什么数据需要加锁

内核全局变量

共享数据

死锁;所有线程都在等待对方释放锁,任何线程都无法继续进行

2. 内核同步方法

2.1 原子操作

原子操作保证指令以原子方式执行

原子操作通常是内联函数,通过内嵌汇编指令完成

原子操作比其他同步方法给系统的开销小

linux内核提供对整数和单独对位进行的原子操作

整数原子操作相关函数为asm/atomic.h文件中

位原子操作相关函数为asm/bitops.h

2.2 自旋锁

自旋锁(spin lock)最多只能被一个可执行线程持有。如果锁未被占用,线程立刻可以得到。如果被占用,会一直循环等待锁可用。

自旋锁可用于防止多个线程同时进入临界区

自旋时特别浪费cpu,所以不应该被长时间持有

接口定义在<linux/spinlock.h>,具体与体系结构相关的实现在<asm/spinlock.h>

linux中自旋锁不可用递归

自旋锁的使用spinlock_t mr_lock = SPIN_LOCK_UNLOCKED; //普通请求锁 spin_lock(&mr_lock); //禁止中断请求锁 spin_lock_irqsave(&mr_lock); //确保中断是激活的情况可用的方法 spin_lock_irq(&mr_lock) /**临界区**/ spin_unlock_irq(&mr_lock) spin_unlock_irqrestore(&mr_lock); spin_unlock(&mr_lock)

自旋锁可以用在中断处理程序中(信号量不行,会导致休眠),在使用锁之前,要禁止本地中断,否则会导致死锁

2.3 读写自旋锁

锁用途可以明确分为读锁和写锁。

一个或多个读任务可以并发的持有读者锁

用于写的锁只能被一个写任务持有,且此时不能并发读

读写锁使用rwlock_t mr_rwlock = RW_LOCK_UNLOCKED; read_lock(&mr_rwlock); /**只读临界区**/ read_unlock(&mr_rwlock) write_lock(&mr_rwlock) /**读写临界区**/ write_unlock(&mr_rwlock)

不能把读锁升级为写锁,会导致死锁

2.4 信号量

信号量是一种睡眠锁

同一时刻允许任意数量的锁持有者

信号量数量为1时,称为二值信号量或互斥信号量

如果一个任务试图获取被占用的信号量,信号量会将其推入等待队列,让其睡眠。当持有信号量的进程释放后,等待的任务被唤醒,获得信号量

信号量的特点

适合锁会被长时间持有的情况

锁被短时间持有,睡眠耗时可能比全部时间还长

会睡眠,不能在中断中调用

占有信号量时不能占用自旋锁。自旋锁不允许休眠

信号量支持两个原子操作P和V,荷兰语的探查和增加

信号量相关文件:<asm/semaphore.h>

使用信号量//声明信号量 static DECLARE_SEMAPHORE_GENERIC(name, count); //声明互斥量 static DECLARE_MUTET(name); //以指针方式初始化信号量 sema_init(sem, count); //以指针方式初始化互斥量 init_MUTET(sem); //试图获得信号量 down_interruptible(&name) //释放信号量 up(&name)

2.5 读写信号量

与读写锁一样

相关文件:<linux/rwsem.h>

2.6 完全变量

提供代替信号量的简单解决方法

相关文件:<linux/completion>

2.7 Seq锁

提供一种简单的机制,用于读写共享数据

内部实现主要一个序列计数器

锁的初始化为0,写数据时,会得到锁,且序列值增加。读数据之前和之后,序列号被读取,如果相同则没有被打断,如果为偶数则没有写操作发生

2.8 屏障

屏障(barriers)可以确保指令顺序执行,禁止指令重排序

重排序是因为现代处理器为了优化其传送管道,打乱了分派和提交指令的顺序

rmb方法提供“读”内存屏障,rmb前面的载入操作不会排在rmb之后,反之亦然

wmb方法提供“写”内存屏障,wmb前面的存储操作不会排在wmb之后。

mb方法提供了“读写”屏障

七. 内存管理

1. 内核对内存的管理

1.1 页

内核把物理页作为内存管理的基本单元。

内存管理单元:MMU,管理内存,并把虚拟地址转换为物理地址的硬件

页大小跟体系结构不同而不同,大多数32为体系支持4KB的页,64位体系支持8KB的页

物理页的数据结构位于<linux/mm.h>中的struct page。page与物理页相关,而不是虚拟页sturct page{ unsigned long flags; // 页的状态,是否脏,是否被锁定。可表示32种状态。定义与<linux/page-flags.h> atomic_t count; // 页的引用计数,被使用了多少次。为0时,新的分配就可以使用它 struct list_head list; struct address_space *mapping; //指向与该页有关的address_space对象 unsigned long index; struct list_head lru; union{ struct pte_chain *chain; pte_addr_t direct; }pte; unsigned long private; void *virtual; //页虚拟地址,虚拟内存地址 }

1.2 区

由于硬件限制,有些页位于内存中特定的物理地址上,不能用于特定的任务,所以把也划分为不同的区(zones)。

区对有相似特性的页进行分组。区的划分没有任何物理意义,仅仅是为了管理页采取的逻辑分组

linux使用三种区:

ZONE_DMA:能执行DMA(直接内存访问)的区

ZONE_NORMAL:能正常映射的区

ZONE_HIGHEM:不能永久映射到内核地址空间的“高端内存”

区的数据结构位于<linux/mmzone.h>的struct zone

2. 页相关接口

内核提供了一种请求内存的底层机制,提供了访问接口,接口都以页为端午分配内存,定义与<linx/gfp.h>中// 分配2^order个连续的物理页,并返回指针 struct page* alloc_pages(unsigned int gfp_mask, unsigned int order) // 将页转换为逻辑地址,页是连续的,其他页紧随其后 unsigned long __get_free_pages(unsigned int gfp_mask, unsigned int order) // 只获取一页,高端地址分配需要使用此函数 struct page* alloc_page(unsigned int gfp_mask) unsigned long get_free_page(unsigned int gfp_mask) //获取填充内容为0的页 unsigned long get_zeroed_page(unsigned int gfp_mask) //释放页 void __free_pages(struct page *page, unsigned int order) void free_pages(unsigned long addr, unsigned int order) void free_page(unsigned int order) // 与用户空间的malloc函数类似,最通用的接口。提供用于获得以字节为单位的一块内核内存 // 定义与<linux/slab.h>中 void *kmalloc(size_t siez, int flags) // kmalloc相反函数,释放空间 void kfree(const void *ptr) // 确保分配的页在物理地址上是连续的 // 定义与<linux/vmalloc.h> void* vmalloc(unsigned long size) //释放空间 void vfree(void *addr)

gfp_mask标志,在<linx/gfp.h>中,分为三类:行为修饰符,区修饰符和类型

行为修饰符:内核如何分配所需的内存

区修饰符:从哪儿分配内存

类型:组合了行为修饰符和区修饰符

3. slab

slab提供通用数据结构缓存层的角色,slab会给每个处理器维持一个对象告诉缓存(空闲链表)

适用于需要创建和销毁很大的数据结构

slab层把不同的对象划分为所谓告诉缓存组,每个告诉缓存都存放不同类型的对象(每种对象类型对应一个高速缓存)

每个slab处于三种状态之一:慢,部分或空

多个salb组成一个高速缓存,各种数据结构:

slab的:struct slab

高速缓存:kmem_cache_s

满链表:slabs_full

部分链表:slabs_partial

空链表:slabs_empty

4. 高端内存的映射

高端内存的页不能永久映射到内核地址空间中,所以某种标志获得的页不可能有逻辑地址

x86系统上,高端内存中的页被映射到3GB-4GB的逻辑地址

映射相关接口://映射一个给定的page结构到内核地址空间: void kmap(sturct page *page) //解除映射关系 void kunmap(struct page* page) //临时映射 void *kmap_atomic(sturct page *page, enum km_type type)

八. 虚拟文件系统

1. 基本概念

虚拟文件系统:VFS,提供文件系统接口。通过VFS,可以利用标准的unix文件系统调用堆不同介质的不同文件进行操作

linux支持相当多的文件系统(超过50种):

本地文件系统: ext2, ext3

网络文件系统:NFS,Coda

VFS中有四个主要的对象类型

超级快对象,代表一个已安装文件系统

索引节点对象,代表一个文件

目录项对象,代表一个目录项

文件对象,代表由进程打开的文件

2. 超级块对象

各种文件系统都必须实现超级块,该对象用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统控制块

超级块数据结构定义与<linux/fs.h>中的super_block。

超级块中的s_op指向超级快的操作函数表,由super_operation结构体表示。文件系统对超级块操作时,会找到相应的操作方法

也就是不同的文件系统的信息,通过往super_operation中注册自己的针对文件系统操作的方法,提供给VFS使用

超级快相关代码位于<fs/super.c>中

3. 索引节点对象

索引节点对象包含了内核在操作文件或目录时需要的全部信息

索引节点对象数据结构位于<linux/fs.h>中的struct inode

代表了文件系统中的一个文件(包括普通文件,管道等等)

索引节点中的inode_operations项也非常重要,定义了操作索引节点对象的所有方法

4. 目录项对象

目录项包括执行目录相关的操作,比如路径名查找等

目录项数据结构位于<linux/dcache.h>中的struct dentry

目录项状态包括:被使用,未被使用和负状态

目录项还包括目录项缓存,包括:

被使用的目录项链表

最近被使用的双向链表

哈希表和相应的哈希函数用来快速将给定路径解析为相关目录项对象

目录项操作定义在dentry_operation结构体中,位于<linux/dcache.h>

5. 文件对象

文件对象定义与<linux/fs.h>中的struct file结构体

文件操作由file_operations结构体表示

具体的文件系统定义不同的实现

6. 其他数据结构

与文件系统相关的数据结构:struct file_system_type,描述特定文件系统类型,如ext3或XFS

安装文件系统的实例:vfsmount,<linux/mount.h>

进程描述符的files指向的数据,包含进程相关的打开的文件及文件描述符的等信息:struct files_struct,<linux/file.h>

进程描述符的fs指向的数据,包含文件系统和进程信息:struct fs_struct, <linx/fs_struct.h>

进程描述符的namespace指向的数据,包含命名空间信息:struct namespace, <linx/namespace.h>

九. 块IO层

1. 基本概念

基本设备类型包括:块设备,字符设备。区别在于是否可以被随机访问

块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,最常见的大小是512字节

物理磁盘按扇区寻址,文件系统按块进行访问,块是较高层次的抽象

块包含一个或多个扇区,但大小不超过一页

块调入内存时,需要先加载到缓冲区,每个缓冲区与一个块对应

每个缓冲区有一个描述符,用buffer_head结构表示,称为缓冲区头。在文件<linux/buffer_head.h>中。包括缓冲区状态,使用计数,逻辑块号,物理页,块大小等信息

2. bio

目前内核中块IO操作基本容器由bio结构表示,位于<linux/bio.h>

3. io调度程序

十. 进程地址空间

1. 基本概念

每个进程都有唯一的地址空间,彼此之间互不干扰

进程只能访问有效范围内的内存地址

内存区域包含各种内存对象,包括:

代码段:可执行文件代码的内存映射

数据段:已初始化全局变量的内存映射

BSS段:未初始化全局变量

其他

2. 内存描述符

内核使用内存描述符表示进程的地址空间

内存描述符数据结构位于<linux/sched.h>中的mm_struct

内部的mmap和mm_rb两个数据结构表示的内容一样,此处做了冗余。前者为链表,便于高效遍历所有;后者为红黑树,便于高效搜索

锁都有的mm_struct都通过自身的mmlist链接在一个双向链表中,首元素的init进程,init_mm描述符。操作链表时,要用mmlist_lock加锁,锁位于<kernel/fork.c>

进程描述符中的mm字段存放内存描述符

分配内存描述符:copy_mm,内部调用allocate_mm宏从mm_cachep slab缓存分配

内核线程没有进程地址空间,mm字段为空

3. 内存区域

内存区域由vm_area_struct表示,定义在<linux/mm.h>中,也称虚拟内存区域

vm_area_struct描述了指定地址空间内连续空间上的一个独立内存范围

vma包括很多标志位,标志了所含页面的行为和信息

VM_READ:页面读权限

VM_WRITE:页面写权限

VM_EXEC:页面可执行权限

vm_area_struct结构体的vm_ops指向与指定内存区域相关的操作函数表,由vm_operations_struct表示

十一. 页高速缓存和页回写

1. 基本概念

页高速缓存是linux实现的一种磁盘缓存,主要用来减少对磁盘的io操作

通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理内存的访问

磁盘高速缓存的意义:

加快访问速度,内存速度大于磁盘

临时局部原理:数据一旦被访问,很有可能在短期内再次被访问

执行io操作前,内核会检查数据是否已经在页高速缓存中,如果在就可以立马返回

2. 页高速缓存数据结构

页高速缓存使用address_space结构体描述页高速缓存中的页面,定义与<linux/fs.h>中

内部的a_ops指向地址空间对象中的操作函数表,由address_space_operations结构体表示

检查页是否在高速缓存中操作必须高效,每个address_struct提供的基树(radix tree),page_tree的二叉树,记录了文件偏移量。以达到快速检索的目的。基树代码在<lib/radix-tree.c>

3. 页回写

pdflush后台例程负责将内存的脏页写回磁盘,写的时机包括

空闲内存低于某个值,以释放内存。阀值可配置

脏页驻留时间超过特定值

周期性回写,防止系统异常数据丢失

pdflush代码在<mm/pdflush.c>中。回写机制代码在<mm/page-writeback.c>,<fs/fs-writeback.c>

十二. 其他

定时器和时间管理

周期性时间由系统定时器驱动,是一种可编程硬件芯片,能以固定频率产生中断(定时器中断)

实际时间:定义在<kernel/time.c>中。struct timespec xtime; timespec数据结构定义在文件<linux/time.h>中

定时器:定义在<linux/timer.h>中,由结构time_list

原文作者:首页 - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛

原文链接:linux内核设计与实现 - 圈点 - 内核技术中文网 - 构建全国最权威的内核技术交流分享论坛(版权归原文作者所有,侵权留言联系删除)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

linux内核设计与实现 的相关文章

  • Java多线程之~~~~使用wait和notify实现生产者消费者模型

    在多线程开发中 xff0c 最经典的一个模型就是生产者消费者模型 xff0c 他们有一个缓冲区 xff0c 缓冲区有最大限制 xff0c 当缓冲区满 的时候 xff0c 生产者是不能将产品放入到缓冲区里面的 xff0c 当然 xff0c 当
  • 拷贝、移动构造

    include lt iostream gt using namespace std class temp private char a public temp char c char b a 61 new char 2 a 0 61 c
  • Dockerfile

    Dockerfile Dockerfile简介1 Dockerfile基本介绍2 Dockerfile构建过程3 Dockerfile指令用法 Dockerfile构建镜像1 Dockerfile使用CentOS构建apache镜像1 1相
  • MySQL数据库备份与恢复

    在项目的开发过程中数据库的备份是非常重要的 xff0c 为了防止数据库受到破坏 xff0c 造成不可估量的损失 xff0c 所以一定要进行数据库的备份 xff0c 并且需要掌握数据库恢复方法 xff0c 在发生数据库损坏的时候 xff0c
  • Linux设备驱动基础知识

    一 认识驱动 1 什么是驱动 不知道的情况下 xff0c 我们一般会去问度娘 xff0c 但是度娘的这个回答比较专业化 xff0c 对小白来说并不容易理解 xff0c 依旧不明白驱动到底是个啥 度娘 xff1a 驱动 xff0c 计算机软件
  • 接口及实现方法

    什么是接口 接口 xff08 英文 xff1a Interface xff09 xff0c 在JAVA编程语言中是一个抽象类型 xff0c 是抽象方法的集合 xff08 接口中的所有的方法都是抽象方法 xff09 xff0c 接口通常以in
  • JS中Document对象

    Document对象 Document对象就是一个标记性文档对象 它就是HTML文件本身 当浏览器把一个html文件加载到内存中之后 xff0c 在内存中就会形成这个document对象 document URL xff1a 返回当前页面的
  • Sphinx环境配置以及VScode编写Rst文档转html

    Sphinx环境配置 安装python3通过python3安装sphinx sphinx官方网站 xff1a Installing Sphinx Sphinx documentation python官方网站 xff1a Welcome t
  • js实现防抖和节流

    防抖 xff1a 防抖就是在连续多次触发该事件 xff0c 事件触发间隔小于设定的事件 xff0c 事件只会执行最后一次 节流 xff1a 规定的时间内 xff0c 只能触发一次该事件 接下来看代码 xff1a lt DOCTYPE htm
  • 树莓派系统镜像一键瘦身备份脚本, 生成最小化镜像img(一)

    开发 测试环境 树莓派 xff1a Raspberry Pi 3b 43 TF 存储卡 xff1a 32GB操作系统 xff1a 2020 05 27 raspios buster full armhf xff08 官方版本 xff09 程
  • 分享——网页上的虚拟机

    大家还记得之前的Windows系统吗 xff1f 相信大家一定想要重温或体验以前Windows的系统吧 xff0c 可有些朋友想体验但又嫌装虚拟机太麻烦 xff0c 怎么办呢 其实 xff0c 还有 网上的虚拟机 现在 xff0c 我就给大
  • 单片机编程中的裸机编程和多任务系统FreeRTOS系统详解,以及怎么学习FreeRTOS,看哪家的教程?(合集)

    单片机编程中的裸机系统和多任务系统 学习了那么久的stm32还停留在裸机 xff1f xff1f xff1f 单片机编程中的裸机系统和多任务系统 1 裸机系统1 1轮询系统1 2 前后台系统 2 多任务操作系统3 为什么要学习多任务操作系统
  • Selenium+Pytest自动化测试框架实战,还不会点这里一清二楚,全网最细教程!

    如果下方文字内容没有看明白的话 xff0c 我推荐大家看一套视频 xff0c 比文字内容讲的更加详细 xff01 在华为工作了10年的大佬出的Web自动化测试教程 xff0c 华为现用技术教程 xff01 哔哩哔哩 bilibili 在华为
  • Firewalld防火墙基础

    目录 前言 一 概述 2 Firewalld和iptables的关系 2 1 Firewalld和iptables的关系 3 Firewalld区域 3 1 firewalld区域的概念 3 2 firewalld防火墙定义了9个区域 3
  • Matlab/simulink控制,遗传pid,模糊pid,滑模控制,自抗扰ADRC控制

    Matlab simulink控制 xff0c 遗传pid 模糊pid xff0c 滑模控制 xff0c 自抗扰ADRC控制 xff0c 鲁棒控制 xff0c LADRC控制等 xff0c 以上控制均已封装为simulink模块 xff0c
  • USB串口转CAN通讯盒资料

    公司开发低成本USB串口转CAN通讯盒 xff0c 其他项目优先 xff0c 停止开发 xff0c 自己闲暇时间继续开发而成 出资料 主控是stm32f103c8t6 整套资料 xff0c 很适合初学者学习上下位机通讯控制入门 xff0c
  • I2C读取IST8310

    文章目录 I2C简介I2C传输过程I2C仲裁IST8310磁力计Cube配置相关函数HAL I2C Mem Read函数HAL I2C Mem Write函数IST8310的初始化 程序流程代码编写 I2C简介 2C是一种半双工双向二线制的
  • 开发型C板IST8310磁力计的读写过程

    文章目录 读取单字节过程读取多字节过程写入单字节过程写入多字节过程 对于开发板C型的IST8310 xff0c I2C的地址是0x0E xff0c 读取0x00寄存器的值 读取单字节过程 发送一个起始信号 xff08 如图一 xff09 发
  • 嵌入式学习——I2C总线通信协议

    目录 1 I2C协议简介 I2C协议概述 I2C协议的物理层 I2C协议层 硬件I2C 软件模式的I2C协议 2 编程例题 题目要求 代码以及运行效果 3 总结 1 I2C协议简介 I2C协议概述 I2C通讯协议 Inter xff0d I
  • 善用 $ROS_DISTRO 简化ros程序安装

    查看ros版本信息 xff1a echo ROS DISTRO 在我的环境下 xff0c ROS DISTRO 61 foxy xff0c 可以把 ROS DISTRO就当成foxy字符串来用 我们在安装ros程序的时候 xff0c 很多时

随机推荐