- 学习视频来自于B站【小白入门 通俗易懂】2021韩顺平 一周学会Linux。
- 可能会用到的资料有如下所示,下载链接见文末:
- 《鸟哥的Linux私房菜 基础学习篇 第四版》1
- 《鸟哥的Linux私房菜 服务器架设篇 第三版》2
- 《韩顺平_2021图解Linux全面升级》3
- 《Linux0.01内核源代码及注释》4
首先给出几个理由:
- 爱好,就是喜欢linux(黑客精神),想深入理解linux底层运行机制,对操作系统有深入理解。
- 找工作面试的需要。作为开发者,不管你从事的是驱动开发,应用开发还是后台开发,你都需要了解操作系统内核的运行机制,这样才能写出更好的代码。作为开发人员不应该只局限在自己的领域,你设计的模块看起来小,但是你不了解进程的调用机制,你不知道进程为什么会阻塞、就绪、执行几个状态。那么很难写出优质的代码。
老韩忠告,作为有追求的程序员,还是应该深入的了解一个操作系统(比如linux/unix)的底层机制,最好是源码级别的,这样你写多线程高并发程序,包括架构、优化、算法等,思想高度不一样的。当然也不是要求大家把一个非常庞大的Linux内核每一行都读懂,但至少能看几个核心的模块。
很多人害怕读Linux内核,Linux内核这样大而复杂的系统代码,阅读起来确实有很多困难,但是也不是想象的那么高不可攀。内核源代码有很多版本,2020年Linux的内核源码就至少有2700w行代码,可读性就非常差,所以建议从linux-0.01入手,总共的代码1w行左右。下面是Linux内核的下载地址:
在阅读源码前,需要注意:
1.linux0.01的阅读需要懂c语言。
2.阅读源码前,应知道inux内核源码的整体分布情况。现代的操作系统一般由进程管理、内存管理、文件系统、驱动程序和网络等组成。Linux内核源码的各个目录大致与此相对应。
3.在阅读方法或顺序上,有纵向与横向之分。所谓纵向就是顺着程序的调用顺序逐步进行;所谓横向,就是按模块进行。它们经常结合在一起进行。
4.对于Linux启动的代码可顺着Linux的启动顺序一步步来阅读;对于像内存管理部分,可以单独拿出来进行阅读分析。实际上这是一个反复的过程,不可能读一遍就理解。注:Linux0.01大概是1991年左右写的,那时候网络不发达,驱动程序也几乎没有,所以Linux0.01相对来说还是比较简单的。
1. 源码目录介绍
linux内核源码阅读&目录介绍&main.c说明
按照上一小节的方法下载linux-0.01源码后,打开目录查看,如下图所示:
下面这些目录/文件的含义(chatgpt):
- boot:包含了引导加载程序的代码,用于启动内核。这里的代码负责在系统引导时加载内核到内存中并启动它。
- fs:这个目录包含了文件系统(file system)相关的代码。linux-0.01 内核支持一些基本的文件系统操作,但功能相对有限。
- include:包含了一些头文件,这些头文件定义了内核中使用的常量、数据结构、函数原型等。这些头文件在整个内核代码中被广泛引用。
- init:包含了内核的初始化代码,涉及系统的启动和初始化过程。包含核心的初始化代码
main.c
。- kernel:这是内核的主要代码目录,包含了实现操作系统核心功能的代码,例如进程调度、系统调用、中断处理等。
- lib:这个目录包含了一些通用的函数库和工具函数,用于辅助内核的开发。
- mm:这个目录包含了内存管理(memory manager)相关的代码。内存管理是操作系统的一个关键部分,它负责管理系统的内存资源,包括分配、释放和保护内存区域。
- tools:这个目录包含了一些工具和脚本,用于构建、编译和调试内核。
- Makefile:编译文件,有多个,每个模块都可能对应一个Makefile。
为了方便阅读源码,现在将整个源码文件夹linux-0.01,使用Xftp上传到CentOS的/opt/linuxsrc/
目录下,然后就可以很方便的查看,比如下面使用tree
指令查看整个目录的所有文件:
# 首先安装一下tree
[root@CentOS76 ~]# cd /opt/linuxsrc
[root@CentOS76 linuxsrc]# tree linux-0.01/
bash: tree: 未找到命令...
[root@CentOS76 linuxsrc]# sudo yum install tree
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
# 安装过程略,遇到什么选项输入y同意就行
已安装:
tree.x86_64 0:1.6.0-10.el7
完毕!
# 查看源码文件夹的目录树
[root@CentOS76 linuxsrc]# tree linux-0.01/
linux-0.01/
├── boot
│ ├── boot.s
│ └── head.s
├── fs
│ ├── bitmap.c
│ ├── block_dev.c
│ ├── buffer.c
│ ├── char_dev.c
│ ├── exec.c
│ ├── fcntl.c
│ ├── file_dev.c
│ ├── file_table.c
│ ├── inode.c
│ ├── ioctl.c
│ ├── Makefile
│ ├── namei.c
│ ├── open.c
│ ├── pipe.c
│ ├── read_write.c
│ ├── stat.c
│ ├── super.c
│ ├── truncate.c
│ └── tty_ioctl.c
├── include
│ ├── a.out.h
│ ├── asm
│ │ ├── io.h
│ │ ├── memory.h
│ │ ├── segment.h
│ │ └── system.h
│ ├── const.h
│ ├── ctype.h
│ ├── errno.h
│ ├── fcntl.h
│ ├── linux
│ │ ├── config.h
│ │ ├── fs.h
│ │ ├── hdreg.h
│ │ ├── head.h
│ │ ├── kernel.h
│ │ ├── mm.h
│ │ ├── sched.h
│ │ ├── sys.h
│ │ └── tty.h
│ ├── signal.h
│ ├── stdarg.h
│ ├── stddef.h
│ ├── string.h
│ ├── sys
│ │ ├── stat.h
│ │ ├── times.h
│ │ ├── types.h
│ │ ├── utsname.h
│ │ └── wait.h
│ ├── termios.h
│ ├── time.h
│ ├── unistd.h
│ └── utime.h
├── init
│ └── main.c
├── kernel
│ ├── asm.s
│ ├── console.c
│ ├── exit.c
│ ├── fork.c
│ ├── hd.c
│ ├── keyboard.s
│ ├── Makefile
│ ├── mktime.c
│ ├── panic.c
│ ├── printk.c
│ ├── rs_io.s
│ ├── sched.c
│ ├── serial.c
│ ├── sys.c
│ ├── system_call.s
│ ├── traps.c
│ ├── tty_io.c
│ └── vsprintf.c
├── lib
│ ├── close.c
│ ├── ctype.c
│ ├── dup.c
│ ├── errno.c
│ ├── execve.c
│ ├── _exit.c
│ ├── Makefile
│ ├── open.c
│ ├── setsid.c
│ ├── string.c
│ ├── wait.c
│ └── write.c
├── Makefile
├── mm
│ ├── Makefile
│ ├── memory.c
│ └── page.s
└── tools
└── build.c
11 directories, 88 files
2. 主函数介绍
上一小节已经提到源码目录下的init
文件夹便包括了内核的主函数main.c
。本小小节就来简单介绍一下main.c
中的入口函数void main()
。首先给出main.c
的全部内容(无注释):
* calls - which means inline code for fork too, as otherwise we
* would use the stack upon exit from 'fork()'.
*
* Actually only pause and fork are needed inline, so that there
* won't be any messing with the stack from main(), but we define
* some others too.
*/
static inline _syscall0(int,fork)
static inline _syscall0(int,pause)
static inline _syscall0(int,setup)
static inline _syscall0(int,sync)
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
static char printbuf[1024];
extern int vsprintf();
extern void init(void);
extern void hd_init(void);
extern long kernel_mktime(struct tm * tm);
extern long startup_time;
/*
* Yeah, yeah, it's ugly, but I cannot find how to do this correctly
* and this seems to work. I anybody has more info on the real-time
* clock I'd be interested. Most of this was trial and error, and some
* bios-listing reading. Urghh.
*/
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \
inb_p(0x71); \
})
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
static void time_init(void)
{
struct tm time;
do {
time.tm_sec = CMOS_READ(0);
time.tm_min = CMOS_READ(2);
time.tm_hour = CMOS_READ(4);
time.tm_mday = CMOS_READ(7);
time.tm_mon = CMOS_READ(8)-1;
time.tm_year = CMOS_READ(9);
} while (time.tm_sec != CMOS_READ(0));
BCD_TO_BIN(time.tm_sec);
BCD_TO_BIN(time.tm_min);
BCD_TO_BIN(time.tm_hour);
BCD_TO_BIN(time.tm_mday);
BCD_TO_BIN(time.tm_mon);
BCD_TO_BIN(time.tm_year);
startup_time = kernel_mktime(&time);
}
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
time_init();
tty_init();
trap_init();
sched_init();
buffer_init();
hd_init();
sti();
move_to_user_mode();
if (!fork()) { /* we count on this going ok */
init();
}
/*
* NOTE!! For any other task 'pause()' would mean we have to get a
* signal to awaken, but task0 is the sole exception (see 'schedule()')
* as task 0 gets activated at every idle moment (when no other tasks
* can run). For task0 'pause()' just means we go check if some other
* task can run, and if not we return here.
*/
for(;;) pause();
}
static int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
write(1,printbuf,i=vsprintf(printbuf, fmt, args));
va_end(args);
return i;
}
static char * argv[] = { "-",NULL };
static char * envp[] = { "HOME=/usr/root", NULL };
void init(void)
{
int i,j;
setup();
if (!fork())
_exit(execve("/bin/update",NULL,NULL));
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
NR_BUFFERS*BLOCK_SIZE);
printf(" Ok.\n\r");
if ((i=fork())<0)
printf("Fork failed in init\r\n");
else if (!i) {
close(0);close(1);close(2);
setsid();
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
_exit(execve("/bin/sh",argv,envp));
}
j=wait(&i);
printf("child %d died with code %04x\n",j,i);
sync();
_exit(0); /* NOTE! _exit, not exit() */
}
然后是其中的入口函数(有注释):
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
time_init(); //初始化运行时间
tty_init(); //初始化tty终端
trap_init(); //初始化陷阱门(硬件中断向量)
sched_init(); //初始化调度程序
buffer_init(); //初始化缓冲管理
hd_init(); //初始化硬盘
sti(); //所有初始化工作完成后,开启中断
move_to_user_mode(); //进入到相关的用户模式
if (!fork()) { /* we count on this going ok */
init();
}
/*
* NOTE!! For any other task 'pause()' would mean we have to get a
* signal to awaken, but task0 is the sole exception (see 'schedule()')
* as task 0 gets activated at every idle moment (when no other tasks
* can run). For task0 'pause()' just means we go check if some other
* task can run, and if not we return here.
*/
for(;;) pause();
}
大致的介绍就到这里,后续可以自行结合文档《Linux0.01内核源代码及注释.pdf》4深入阅读。注意不要求逐行阅读,先看个大概即可,注意要反复地看。
在最开始介绍Linux系统时便提到,严格来说Linux只是一个内核,只不过在这个内核的基础上添加各种不同的软件和工具,才有了许许多多的Linux发行版,所以Linux内核对于Linux系统来说至关重要。Linux内核不断进行定期更新,包括错误修复、增强高级功能以及许多其他增强功能。虽然没有人会强迫你更新到最新的Linux内核,但以下是进行内核升级的几个理由:
- 性能提升:新的内核可能会提供更好的性能,包括更快的启动时间、更快的文件系统访问和更快的网络传输速度等。
- 安全性提升:新的内核通常修复了已知的漏洞和安全问题,提高了系统的安全性和稳定性。
- 新的硬件支持:新的内核可能支持更多的硬件设备和技术。
- 新的功能支持:新的内核通常会提供新的功能和改进,例如更好的文件系统支持、更好的虚拟化支持等。
【参考1】CSDN博文“Centos7系统内核如何更换?为什么需要更新内核?”。
【参考2】PHP中文网“您应该更新 Linux 内核的五个理由”。
本小节首先来介绍一下最新版(2023/8/25)的Linux内核目录。首先是下载解压Linux内核:
################## 指令速览 #####################
cd /opt/linuxsrc
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.4.11.tar.gz
tar -zxvf linux-6.4.11.tar.gz
# 注:若下载速度太慢,也可以直接在浏览器复制上述网址下载,然后通过Xftp上传到Linux的/opt/linuxsrc目录下
################## 实际演示 #####################
[root@CentOS76 linuxsrc]# cd /opt/linuxsrc
[root@CentOS76 linuxsrc]# wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.4.11.tar.gz
--2023-08-25 09:44:47-- https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.4.11.tar.gz
正在解析主机 cdn.kernel.org (cdn.kernel.org)... 151.101.109.176, 2a04:4e42:8c::432
正在连接 cdn.kernel.org (cdn.kernel.org)|151.101.109.176|:443... 已连接。
已发出 HTTP 请求,正在等待回应... 200 OK
长度:221659176 (211M) [application/x-gzip]
正在保存至: “linux-6.4.11.tar.gz.1”
12% [======> ] 27,768,960 7.32MB/s 剩余
# 剩下的下载页面就不显示了,遇到选项就选择默认即可。
[root@CentOS76 linuxsrc]# tar -zxvf linux-6.4.11.tar.gz
# 解压过程省略,会稍微等一会
linux-6.4.11/virt/lib/
linux-6.4.11/virt/lib/Kconfig
linux-6.4.11/virt/lib/Makefile
linux-6.4.11/virt/lib/irqbypass.c
[root@CentOS76 linuxsrc]# ls
linux-0.01 linux-6.4.11 linux-6.4.11.tar.gz
[root@CentOS76 linuxsrc]# ls linux-6.4.11
arch COPYING Documentation include ipc kernel MAINTAINERS net samples sound virt
block CREDITS drivers init Kbuild lib Makefile README scripts tools
certs crypto fs io_uring Kconfig LICENSES mm rust security usr
可以看到相比于linux-0.01,linux-6.4.11多出来非常多的内容。下面就来介绍linux-6.4.11源码中各目录的含义:
[root@CentOS76 linuxsrc]# ls linux-6.4.11
arch COPYING Documentation include ipc kernel MAINTAINERS net samples sound virt
block CREDITS drivers init Kbuild lib Makefile README scripts tools
certs crypto fs io_uring Kconfig LICENSES mm rust security usr
- arch:包含体系结构特定的代码,其中每个子目录代表一个特定的硬件体系结构(例如x86、ARM、PowerPC等)。
- COPYING:可能是一个包含 Linux 内核的版权信息和许可证条款的文件。
- Documentation:包含了内核的文档,其中包括关于内核配置、开发指南、API文档等内容。
- include:包含内核头文件,定义了内核中使用的常量、数据结构和函数原型。
- ipc:包含进程间通信(IPC)相关的代码,支持不同的进程间通信机制。
- kernel:包含核心内核代码,涵盖进程调度、中断处理、系统调用等。
- MAINTAINERS:可能是一个包含维护者列表的文件,列出了负责维护和审核特定代码部分的开发者。
- net:包含网络协议栈的代码,涉及网络协议和网络设备驱动程序。
- samples:包含示例代码的目录,这些示例代码可以用来演示如何使用某些内核功能或特性。
- sound:包含声音子系统的代码,支持音频设备和功能。
- virt:包含虚拟化相关的代码,支持不同的虚拟化平台。
- block:包含块设备层的代码,涉及与块设备(如硬盘)相关的逻辑。
- CREDITS:通常包含了对于内核开发和贡献的致谢名单。
- drivers:这是一个庞大的目录,包含了各种硬件设备的驱动程序,例如网络、存储、显卡等。
- init:包含内核初始化代码,涉及启动过程和系统初始化。但不像linux-0.01的
init目录
只包含main.c
,还因为版本升级而包含很多其他的文件。- Kbuild:通常是一个用于构建和编译内核的脚本文件,它可能包含了一些构建规则和选项。
- lib:这个目录包含了一些通用的函数库和工具函数,用于辅助内核的开发。
- README:这个文件通常包含了关于内核的基本信息、编译和安装指南等。
- scripts:可能包含一些用于辅助构建、编译和调试内核的脚本文件。
- tools:包含一些工具和脚本,用于构建、分析和调试内核。
- certs:可能是包含用于存储数字证书的目录,这些证书可能用于验证内核模块或其他安全相关操作。
- crypto:包含加密相关的代码,用于支持不同的加密算法和加密操作。
- fs:包含文件系统的代码,支持各种文件系统,如EXT4、FAT、NTFS等。
- io_uring:可能是与 I/O uring 相关的代码,I/O uring 是一种用于高效异步 I/O 操作的机制。
- Kconfig:通常是用于配置内核选项的配置文件,这些选项可以通过配置工具进行设置。
- LICENSES:可能包含不同的软件许可证文件,用于指定内核中包含的代码的许可证。
- mm:包含内存管理相关的代码,用于管理物理内存和虚拟内存。
- rust:可能包含使用 Rust 编程语言编写的内核模块或代码。
- security:包含安全子系统的代码,用于实施访问控制和安全策略。
- usr:包含用户空间的文件,如测试程序和示例文件。
上一小节已经简单介绍了Linux最新版内核的内容,本小节就来介绍如何对当前的Linux发行版进行内核升级。“内核升级”有专门的指令完成,非常简单,唯一需要我们掌握的知识点是内核的兼容性问题。下表给出了不同的CentOS7发行版与Linux内核的对应关系。
- 维基百科-CentOS(需要梯子)
- 内核版本与发行版本(CentOS & Ubuntu)的对应关系
- Linux内核及主流Linux发行版对应关系汇总
也就是说,内核升级时不能跨越大版本,比如我们当前的版本是CentOS7.6(3.10.0-957),那么内核最多升级到同为CentOS7的CentOS7.9(3.10.0-1160)。而不能选择现在的最新版Linux内核linux-6.4.11(2023-8-25)。
当然正如前面所说,实际上我们在进行内核升级时,无需自己挑选Linux内核,有相应的指令可以帮我们迅速匹配并升级:
################## 指令速览 #####################
uname -a # 查看当前的内核版本
yum info kernel -q # 检测内核版本,显示可以升级的内核
yum update kernel # 升级内核
yum list kernel -q # 查看已经安装的内核
################## 实际演示 #####################
# 1. 查看当前的内核版本
[root@CentOS76 linuxsrc]# uname -a
Linux CentOS76 3.10.0-1160.el7.x86_64 #1 SMP Mon Oct 19 16:18:59 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
# 2. 检测内核版本,显示可以升级的内核
[root@CentOS76 linuxsrc]# yum info kernel -q
已安装的软件包
名称 :kernel
架构 :x86_64
版本 :3.10.0
发布 :1160.el7
大小 :64 M
源 :installed
来自源:anaconda
简介 : The Linux kernel
网址 :http://www.kernel.org/
协议 : GPLv2
描述 : The kernel package contains the Linux kernel (vmlinuz), the core of any
: Linux operating system. The kernel handles the basic functions
: of the operating system: memory allocation, process allocation, device
: input and output, etc.
可安装的软件包
名称 :kernel
架构 :x86_64
版本 :3.10.0
发布 :1160.95.1.el7
大小 :52 M
源 :updates/7/x86_64
简介 : The Linux kernel
网址 :http://www.kernel.org/
协议 : GPLv2
描述 : The kernel package contains the Linux kernel (vmlinuz), the core of any
: Linux operating system. The kernel handles the basic functions
: of the operating system: memory allocation, process allocation, device
: input and output, etc.
# 3. 升级内核
[root@CentOS76 linuxsrc]# yum update kernel
已加载插件:fastestmirror, langpacks
Loading mirror speeds from cached hostfile
* base: mirrors.ustc.edu.cn
* extras: mirrors.ustc.edu.cn
* updates: mirrors.ustc.edu.cn
正在解决依赖关系
--> 正在检查事务
---> 软件包 kernel.x86_64.0.3.10.0-1160.95.1.el7 将被 安装
--> 解决依赖关系完成
依赖关系解决
=======================================================================================================
Package 架构 版本 源 大小
=======================================================================================================
正在安装:
kernel x86_64 3.10.0-1160.95.1.el7 updates 52 M
事务概要
=======================================================================================================
安装 1 软件包
总计:52 M
安装大小:66 M
Is this ok [y/d/N]: y
Downloading packages:
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
正在安装 : kernel-3.10.0-1160.95.1.el7.x86_64 1/1
验证中 : kernel-3.10.0-1160.95.1.el7.x86_64 1/1
已安装:
kernel.x86_64 0:3.10.0-1160.95.1.el7
完毕!
# 4. 查看已经安装的内核
[root@CentOS76 linuxsrc]# yum list kernel -q
已安装的软件包
kernel.x86_64 3.10.0-1160.el7 @anaconda
kernel.x86_64 3.10.0-1160.95.1.el7 @updates
# 5. 查看当前的内核版本
[root@CentOS76 linuxsrc]# uname -a
Linux CentOS76 3.10.0-1160.el7.x86_64 #1 SMP Mon Oct 19 16:18:59 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
# 注意到显示的内核还是升级前的内核,只是下次重启时会选择进入哪个内核
升级内核完成后重启。重启后的页面如下图所示,现在选择哪个,系统都可以正常使用,原来的用户、安装的软件都在,内核升级后完全兼容以前版本的所有功能。
《鸟哥的Linux私房菜 基础学习篇 第四版》 ↩︎
《鸟哥的Linux私房菜 服务器架设篇 第三版》 ↩︎
《韩顺平_2021图解Linux全面升级》 ↩︎
《Linux0.01内核源代码及注释》 ↩︎ ↩︎