略读《Linux内核设计与实现》

略读《Linux内核设计与实现》

用了半个月的时间粗略的过了一遍《Linux内核设计与实现》。确实讲的都是理论的知识,比较通俗易懂吧!这里简单的记录一下,主要的内容,以供后面进一步的学习和回顾。

第二章 从内核出发

本章主要介绍了内核代码的获取,编译,开发注意的事项。

2.1 内核源码树的结构

  • arch:这个为目录存放的是与CPU架构相关的文件,每个子目录代表了一种CPU架构,比如arm,x86,MIPS,PPC等。
  • block:部分块设备的驱动程序。
  • crypto:这个目录下放了一些各种常见的加密算法的C语言代码实现。譬如crc32、md5、sha1等。
  • Documentation:关于内核各部分的通用解释和注释。
  • drivers:驱动目录,里面分门别类的列出了linux内核支持的所有硬件设备的驱动源代码。
  • firmware:固件。某些驱动程序所需的设备固件。
  • fs:fs就是file system,文件系统,里面列出了linux支持的各种文件系统的实现。
  • include:头文件目录,公共的(各种CPU架构共用的)头文件都在这里。
  • init:linux内核启动时初始化内核的代码。(注意不是系统引导代码)
  • ipc:ipc就是inter process commuication,进程间通信的代码。
  • kernel:内核的最核心部分,包括进程调度、定时器等,和平台相关的一部分代码放在arch/*/kernel目录下。
  • lib:一些公用的有用的库函数,注意这里的库函数和C语言的库函数不一样的。在内核编程中是不能用C语言标准库函数,这里的lib目录下的库函数就是用来替代那些标准库函数的。譬如在内核中要把字符串转成数字用atoi,但是内核编程中只能用lib目录下的atoi函数,不能用标准C语言库中的atoi。譬如在内核中要打印信息时不能用printf,而要用printk,这个printk就是我们这个lib目录下的。
  • mm:memory management,内存管理子系统和VM,和平台相关的一部分代码放在arch/*/mm目录下。
  • net:网络相关代码,实现了各种常见的网络协议,譬如TCP/IP协议栈等都在这里。
  • samples:一些例子程序。
  • scripts:用于配置内核文件的脚本文件。
  • security:主要是一个SELinux的模块。
  • sound:常用音频设备的驱动程序等。
  • tools:linux中用到的一些有用工具。
  • usr:目录下是initramfs相关的,和linux内核的启动有关。
  • virt:虚拟化基础结构。
  • Kbuild:kernel build的意思,就是内核编译的意思。这个文件就是linux内核特有的内核编译体系需要用到的文件。
  • Makefile:这个是linux内核的总makefile。

2.2 内核编译

1.内核配置

常用的有:
make menuconfig ## 图形界面配置
make defconfig ## 默认配置
make oldconfig ## 拷贝原来的内核源码树的根目录的 .config 文件用来编译

2.内核编译

make menuconfig
make [-j #]
make modules_install
make install

2.3 其他开发注意事项

1.内核不能使用标准c库
2.不要轻易使用浮点数
3.内核栈很小(之前写驱动的时候,定义了一个很大的数组,把内核栈爆了。)

第三章 进程管理

这一章主要讲了进程的基本概念,进程的状态,进程的创建于销毁。

3.1 进程描述符(process descriptor)

进程描述符对应的一个进程或者是线程,其实质就是 task_struct 结构体。定义在 linux/sched.h 中。

3.2 进程的状态

进程有五种状态:运行(TASK_RUNNING)、可中断(TASK_INTERRUPTIBLE)、不可中断(TASK_UNINTERRUPTIBLE)、被其他进程跟踪(__TASK_TRACED)、停止(__TASK_STOPPED)。
1. R (TASK_RUNNING),可执行状态&运行状态(在run_queue队列里的状态)
2. S (TASK_INTERRUPTIBLE),可中断的睡眠状态, 可处理signal
3. D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态, 可处理signal, 有延迟。不可中断态通常处于的时间较短,等待的事件很快就会出现。
4. T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态, 不可处理signal, 因为根本没有时间片运行代码
5. Z (TASK_DEAD - EXIT_ZOMBIE),退出状态,进程成为僵尸进程。不可被kill, 即不响应任务信号, 无法用SIGKILL杀死

Flow_chart_of _process_states.bmp

3.3 进程的创建与销毁

进程的创建主要有 fork、vfork、exec。进程的终止主要是exit。创建和终止的原理都比较简单,创建时主要是拷贝主要的数据结构,然后终止的时候,这是要注意资源回收。这里主要说说,写时拷贝和孤儿进程和僵尸进程。

写时拷贝(copy-on-write):简单的说就是延时拷贝,当创建一个新的进程时,它们通过共享内存的方式使用同一块内存,当有一个要修改此内存空间的时候,才开始真正的复制一块内存。

进程终止时,资源回收和进程描述符的删除是分开的,先是回收了资源,仅保留了内核栈、thread_info、task_struct 这部分的资源,存在的主要的目的是向父进程提供信息。当父进程收到信息后,才释放这一部分的资源。
如果父进程在子进程之前退出,子进程处于僵尸状态,还保留着这些资源,白白浪费系统资源,所以要为其找新的父进程。找新的父进程的过程,先在线程组(这个概念还没搞懂,尴尬)中找,没找到就给init进程收养。

第四章 进程调度

进程调度,即决定将哪个进程投入到运行。

4.1 IO消耗型和处理器消耗型的进程

IO消耗型的进程:大多数的时间都在等待io,真正需要CPU的时间较少。
处理器型的进程:主要的时间都花在了代码执行上。
有些进程既属于IO消耗型也属于处理器消耗型,如字处理器,既要等到io输入,又可能需要处理器去进行拼音检查或者宏计算。

中文版第三版书中P39页的例子非常好,关于这个CFS算法,对于IO消耗型和服务器型进程的调度,当有io请求时,立刻响应,调度io消耗型的进程,当其运行完,继续等待io时,立刻调度服务器型的进程。虽然在CFS算法中,为它们都分配了50%的CPU时间。

4.2 关于nice值和进程优先级

  1. nice值越小,优先级越高。pri值越高,优先级越高。
  2. Linux有两种实时调度策略 SCHED_FIFO 和 SHCED_RR。还有非实时调度策略 SCHED_NORMAL。
  3. 实时优先级 0-99,非实时的 100-139。nice值表示的是非实时的,范围从-20到+19。映射到实时上就是100-139。
  4. 用ps看到的nice值是经过ps这个程序处理的,但是相对的优先级关系还是一样的,小的nice优先级更高。
  5. nice对应的是相对的处理器时间

4.3 Linux调度器的实现

写这一部分,主要是可以帮助了解调度的关键地方。
实现主要有:
- 时间记账:记录进程运行的时间
- 进程选择:选择下一个执行的进程
- 调度器入口
- 睡眠和唤醒:休眠的进程(中断和不可中断态)在等待某些条件,不应该被调度的,唤醒是当它们等到之后,将其移除等待队列,使其可以被调度。

4.4 其他

  1. 抢占

    • 用户抢占:从内核空间返回用户空间时发生的抢占。从内核返回时,既可以执行本进程,也可以调度新的进程。有两种情况: 从系统调用返回时从中断处理程序返回时
    • 内核抢占:1)中断程序正在执行,且返回内核空间(没懂??); 2)内核代码再次具有可抢占性的时候(preempt_count == 0); 3) 显式调用schedule(); 4) 任务阻塞时。
  2. 绑定处理器的调度
    之前遇到过这样的问题,让一部分程序在特定的核上执行。

第五章 系统调用

主要讲了系统调用的实现,之前做过写过一个系统调用的demo。这里就略掉。

第六章 内核数据结构

内核实现的一些数据结构,知道主要的,后面再用一下。
- 链表 list
- 队列 kfifo
- 映射 idr
- 二叉树 二叉搜索树、红黑树

第七章 中断和中断处理函数

中断的实现,搞了1年,略了。主要是上下半部分的实现,没用过,都是在上半部分做的。
- 中断:由硬件引起的
- 异常:同步中断,由软件引起,如除0,缺页。

7.1 2 3 4是引用的这个博客: https://blog.csdn.net/liusirboke/article/details/49681625

7.1 中断上下文和进程上下文的区别?

1.进程上下文:

(1)进程上文:其是指进程由用户态切换到内核态是需要保存用户态时cpu寄存器中的值,进程状态以及堆栈上的内容,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。

(2)进程下文:其是指切换到内核态后执行的程序,即进程运行在内核空间的部分。

2.中断上下文:

(1)中断上文:硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上文可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境。

(2)中断下文:执行在内核空间的中断服务程序。

7.2 为什么要进行不同之间状态的切换

在现在操作系统中,内核功能模块运行在内核空间,而应用程序运行在用户空间。现代的CPU都具有不同的操作模式,代表不同的级别,不同的级别具有不同的功能,其所拥有的资源也不同;在较低的级别中将禁止使用某些处理器的资源。Linux系统设计时利用了这种硬件特性,使用了两个级别,最高级别和最低级别,内核运行在最高级别(内核态),这个级别几乎可以使用处理器的所有资源,而应用程序运行在较低级别(用户态),在这个级别的用户不能对硬件进行直接访问以及对内存的非授权访问。内核态和用户态有自己的内存映射,即自己的地址空间。

当工作在用户态的进程想访问某些内核才能访问的资源时,必须通过系统调用或者中断切换到内核态,由内核代替其执行。进程上下文和中断上下文就是完成这两种状态切换所进行的操作总称。我将其理解为保存用户空间状态是上文,切换后在内核态执行的程序是下文。

7.3 什么情况下进行用户态到内核态的切换

1.进程上下文主要是异常处理程序和内核线程。内核之所以进入进程上下文是因为进程自身的一些工作需要在内核中做。例如,系统调用是为当前进程服务的,异常通常是处理进程导致的错误状态等。

2.中断上下文是由于硬件发生中断时会触发中断信号请求,请求系统处理中断,执行中断服务子程序。

7.4 中断上下文代码中注意事项

运行于进程上下文的内核代码是可抢占的,但中断上下文则会一直运行至结束,不会被抢占。所以中断处理程序代码要受到一些限制,在中断代码中不能出现实现下面功能的代码:

(1)睡眠或者放弃CPU。
因为内核在进入中断之前会关闭进程调度,一旦睡眠或者放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉。牢记:中断服务子程序一定不能睡眠(或者阻塞)。

(2)尝试获得信号量
如果获得不到信号量,代码就会睡眠,导致(1)中的结果。

(3)执行耗时的任务
中断处理应该尽可能快,因为如果一个处理程序是IRQF_DISABLED类型,他执行的时候会禁止所有本地中断线,而内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。中断处理程序的任务尽可能放在中断下半部执行。

(4)访问用户空间的虚拟地址
因为中断运行在内核空间。

第八章 下半部和推后执行的工作

8.1 什么是下半部?

因为中断处理程序以异步的方式执行。可能打断了重要的代码。同时中断也不能被阻塞。要求中断处理要快速。所以这里把中断分成了两个部分,上半部用来处理和硬件密切相关的操作,下半部用来处理与时间要求不高的任务。中断处理完这个中断处理程序就返回了。

8.2 什么时候使用下半部?

除以下的情况外,其他情况考虑放置在下半部执行。
- 任务对时间非常敏感,放在中断处理程序中;
- 任务和硬件相关,放在中断处理程序中;
- 任务要求不能被其他中断打断,放在中断处理程序中。

8.3 常用的方式

都没有用过,记下来有这几种,后面再看。
- 软中断
- tasklet
- 工作队列

第九、十章 内核同步

9.1 为什么要同步?

主要的原因,多个任务并发执行,并操作了共享数据。而并发执行的原因,是因为抢占和进程调度。常见的并发执行的原因:中断,软中断和tasklet,内核抢占,对称多处理,睡眠及与用户空间的同步(进程睡眠,唤醒一个新的进程开始工作)。

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

9.2 同步方法

主要加锁。但注意死锁。

避免死锁:
1. 顺序加锁
2. 防止发生饥饿
3. 不要重复请求同一个锁
4. 设计简单点

锁的类型:
1. 原子操作
2. 自旋锁
3. 读写自旋锁
4. 信号量
5. 读写信号量
6. 互斥量
7. 完成变量
8. BLK 大内核锁
9. 顺序锁

有一个比较有意思的——顺序:代码不一定按代码的顺序执行的。避免这种情况的有个屏障的机制。

第十一章 定时器和时间管理

主要讲了个定时器,略了。

第十二章 内存管理

越写越觉得这本书写的简单。。(是我飘起来了吗?)

12.1 zone(区)

内核的内存(3G-4G的区域)
- ZONE_DMA : 0-16M
- ZONE_NORMAL : 16-896M
- ZONE_HIGHEM : “高端内存” >896M

12.2 一些分配函数

  • 页的分配和释放:alloc_pages()、free_pages()
  • kmalloc(): 物理内存地址连续
  • vmalloc():物理内存地址不一定连续
  • slab:特别有意思,按数据结构分配

12.3 高端内存映射——永久映射和临时映射

隐约有点理解了为什么分为两种,临时映射是必须的,因为内存不够,必须动态的映射。

第十三章 虚拟文件系统

  • 超级块对象 (文件系统)
  • 索引节点对象 (文件的创建 链接)
  • 目录项对象 (目录)
  • 文件对象 (进程打开的文件)

第十四章 块I/O层

对块设备的操作,对IO的操作很慢,IO调度的主要工作就是为了减少磁盘寻址时间,主要的方式合并与排序。合并相邻的操作。

第十五章 进程地址空间

第十六章 页高速缓存和页回写

也是为了解决磁盘读写慢的缺点,将磁盘的区域映射到内存,然后这里考虑的是什么时候写回到磁盘。(sync的作用)

第十七章 设备与模块

第十八章 调试

  • oops
  • dmesg
  • 神奇的系统请求键 (echo 1 > /proc/sys/kernel/sysrq)
  • gdb kgdb

第十九章 可移植性

  • 字节和数据类型的长度
  • 字节对齐
  • 字节顺序 大端和小端
  • 时间 HZ
  • 页大小

猜你喜欢

转载自blog.csdn.net/thalo1204/article/details/79842704