【Linux】Linux内核结构基础
Linux内核结构基础
- 一、Linux内核结构介绍
- 二、Linux内核结构
- ☆ Linux内核结构框图
- Linux系统架构
- 三、认识驱动
- 为什么学驱动
- 文件名与设备号
- open函数过程实例详解
- 四、shell
- shell脚本
参考博文:
- 【Linux】内核结构(全)
- linux内核结构介绍(简)
- 图解Linux系统的系统架构
- Linux驱动(驱动程序开发、驱动框架代码编译和测试)
一、Linux内核结构介绍
Linux 内核是操作系统的核心部分,它负责管理系统的资源、进程调度、设备驱动程序等核心功能。以下是 Linux 内核的主要组成部分和结构:
1. 进程管理:
- 调度器(Scheduler): 决定在多任务系统中那个进程获得CPU时间
- 进程控制块(Process Control Block,PCB) 存储有关进程的信息,如寄存器状态、进程状态等。
2. 内层管理:
- 虚拟内存管理: 管理进程的虚拟地址空间,包括分页机制、页面置换机制等。
- 物理存储管理: 负责跟踪系统中的物理内存,包括内存分配、释放等。
3. 文件系统:
- 虚拟文件系统(VFS): 提供文件系统的抽象接口,使得不同类型的文件系统可以共享相同的API
- 文件描述符表: 管理打开的文件信息,每个进程都有一个文件描述符表
4. 设备驱动程序:
- 字符设备和块设备驱动: 用于管理字符设备(如终端)和块设备(如硬盘)
- 网络设备驱动: 处理网络设备和协议栈
5. 网络协议栈:
- TCP/IP 协议栈: 包括网络层、传输层等,用于实现网络通信
- 套接字层: 提供应用程序与网络协议直接的接口
6. 系统调用接口:
- 系统调用: 提供用户空间程序与内核直接的接口,允许用户程序请求内核执行特权操作。
7. 中断和异常处理:
- 中断处理程序: 处理硬件中断,允许外部设备向CPU发生信号
- 异常处理程序: 处理由于错误或异常情况而引发的事件
8. 定时器和时钟管理:
- 内核定时器: 用于实现记时和时间管理
- 实时时钟(RTC): 处理硬件时钟
9. 同步和互斥机制:
- 信号量、自旋锁、互斥量等: 用于确保多个进程或线程之间的同步与互斥
10. 系统初始化和启动:
- 引导加载程序(Bootloader): 负责记载内核镜像到内存中
- 启动过程: 初始化硬件、文件系统等,最终执行第一个用户空间进程。
这些组件一起构成了 Linux 内核的核心结构。内核的源代码是 modulized 的,允许添加或删除特定功能的模块,以适应不同的需求。整个内核的结构和实现细节非常庞大,具体的功能和特性取决于具体的内核版本和配置。
linux系统将设备分为3类:字符设备、块设备、网络设备
- 字符设备: 指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后顺序。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
- 块设备: 指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
- 网络设备: 网络设备可以是一个硬件设备,如网卡; 但也可以是一个纯粹的软件设备, 比如回环接口(lo).一个网络接口负责发送和接收数据报文。
二、Linux内核结构
☆ Linux内核结构框图
这一部分内容十分重要,在后续的open函数实例讲解中会有大量的重复,主要是为了突出重要性和强化记忆过程
重点在于理解图的流程
用户态:
- 是指用户编写程序、运行程序的层面,用户态在开发时需要C的基础和C库,C库讲到文件,进程,进程间通信,线程,网络。C库是linux标准库一定有,提供了程序支配内核干活的接口,C库中的各种API调用的是内核态,支配内核干活。
内核态:
-
用户要使用某个硬件设备时,需要内核态的设备驱动程序,进而驱动硬件干活。
-
在linux中一切皆文件,各种的文件和设备(比如:鼠标、键盘、屏幕)都是文件,那既然是文件了,就可以使用文件操作函数来操作这些设备。文件操作函数是如何知道打开的文件是哪一种硬件设备呢?
-
Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在
/dev
目录下,称为设备文件。为了管理这些设备,系统为设备编了号,每个设备号又分为主设备号和次设备号 -
主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。 一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。
-
驱动链表: 管理所有设备的驱动,添加或查找, 添加是发生在我们编写完驱动程序,加载到内核。查找是在调用驱动程序,由应用层用户空间去查找使用open函数。驱动插入链表的顺序由设备号检索,就是说主设备号和次设备号除了能区分不同种类的设备和不同类型的设备,主次设备号还能起到将驱动程序加载到链表的某个位置
综上所述:如果想要打开dev下面的pin4引脚,过程是:
-
用户态调用
open
(“/de/pin4”,O_RDWR); -
对于内核来说,上层调用open函数会触发一个软中断(系统调用专用,中断号是0x80,0x80代表发生了一个系统调用),系统进入内核态,并走到
system_call
,可以认为这个就是此软中断的中断服务程序入口,然后通过传递过来的系统调用号来决定调用相应的系统调用服务程序(在这里是调用VFS中的sys_open
) -
sys_open
会在内核的驱动链表里面根据设备名和设备号查找到相关的驱动函数(每一个驱动函数是一个节点),驱动函数里面有通过寄存器操控IO口的代码,进而可以控制IO口实现相关功能。 -
补充: system_call函数是怎么找到详细的系统调用服务例程的呢? 通过系统调用号查找系统调用表
sys_call_table
!软中断指令INT 0x80运行时,系统调用号会被放入 eax 寄存器中,system_call函数能够读取eax寄存器获取,然后将其乘以4,生成偏移地址,然后以sys_call_table为基址。基址加上偏移地址,就能够得到详细的系统调用服务例程的地址了!然后就到了系统调用服务例程了。
额外补充:
每个系统调用都对应一个系统调用号,而系统调用号就对应内核中的相应处理函数。
所有系统调用都是通过中断0x80来触发的。
使用系统调用时,通过eax 寄存器将系统调用号传递到内核,系统调用的入参通过ebx、ecx……依次传递到内核
和函数一样,系统调用的返回值保存在eax中,所有要从eax中取出
Linux系统架构
- 最内层是硬件,最外层是用户应用,比如浏览器等等。硬件是物质基础,而应用提供服务。
- 为了方便调用内核,Linux将内核的功能接口制作成系统调用(system call)。用户不需要了解内核的复杂结构,就可以使用内核。系统调用是操作系统的最小功能单位。一个操作系统,以及基于操作系统的应用,都不可能实现超越系统调用的功能。
- 系统调用提供的功能非常基础,所以使用起来很麻烦。一个简单的给变量分配内存空间的操作,就需要动用多个系统调用。Linux定义一些库函数(library routine)来将系统调用组合成某些常用的功能。上面的分配内存的操作,可以定义成一个库函数,比如常用的malloc。
三、认识驱动
为什么学驱动
- 以前树莓派开发使用厂家提供的wiringPi库,开发简单。
- 但未来做开发时,不一定都是用树莓派,没有wiringPi库可以用。但只要能运行Linux,linux的标准C库一定有。
- 学会根据标准C库编写驱动,只要能拿到linux内核源码,拿到芯片手册,电路图…就能做开发。
文件名与设备号
nux一切皆为文件,其设备管理同样是和文件系统紧密结合。在目录/dev
下都能看到鼠标,键盘,屏幕,串口等设备文件,硬件要有相对应的驱动,那么open
怎样区分这些硬件呢?
依靠文件名与设备号
依靠文件名与设备号。在/dev
下ls -l
可以看到
设备号又分为:
- 主设备号用于区别不同种类的设备;
- 次设备号区别同种类型的多个设备。
- 驱动插入到链表的位置(顺序)由设备号检索
内核中存在一个内核驱动链表,管理所有设备的驱动。 驱动开发无非以下两件事:
- 编写完驱动程序,加载到内核
- 用户空间open后,调用驱动程序(驱动程序就是操作寄存器来驱动IO口,单片机51,32就是这种操作)
open函数过程实例详解
open
函数是用户空间应用程序通过系统调用访问文件的入口。打通上层到底层硬件的过程涉及几个步骤,主要包括:
1. 用户空间调用: 应用程序在用户空间通过标准库函数 open 调用打开文件。该调用提供了文件路径和一些标志位参数。
int fd = open("/dev/video0", O_RDWR);
2. 系统调用: open
函数触发系统调用,导致应用程序从用户空间切换到内核空间(发生软中断)。在内核空间,调用处理程序开始执行。
3. 查找文件: 内核通过文件系统层次结构查找指定的文件。在这个例子中,是查找 /dev/video0
这个设备文件。
4. 获取文件对象: 找到文件后,内核会创建一个文件对象(file structure)来代表该文件。这个文件对象包含了一系列与文件相关的信息,例如文件的位置、访问模式、设备号等。
5. 权限检查: 内核会检查应用程序对文件的访问权限。如果应用程序没有足够的权限,open
将失败。
6. 分配文件描述符: 成功后,内核为应用程序分配一个文件描述符,这个描述符是一个整数,用于标识文件对象。应用程序将使用这个描述符进行后续的文件操作。
7. 调用文件操作函数: 对于设备文件,内核会调用相应的设备驱动程序中的文件操作函数。例如,open
操作可能触发设备驱动程序的 open
函数。
8. 设备操作: 设备驱动程序中的 open
函数执行与设备相关的初始化或其他必要的操作。这可能包括分配设备资源、配置硬件等。
9. 返回文件描述符: 如果所有步骤成功,open
系统调用返回文件描述符给应用程序,表示文件打开成功。
通过这个过程,open 函数从用户空间向内核发起请求,内核负责管理文件系统和与设备相关的硬件,最终建立起用户空间应用程序与底层硬件的连接。
用户空间调用open:
-
用户空间调用
open
(比如open(“/dev/pin4”,O_RDWR))产生一个软中断(中断号是0x80),进入内核空间调用sys_call
,这个sys_call在内核里面是汇编的,用Source Insight搜索不到。 -
sys_calll
真正调用的是sys_open
(属于VFS层虚拟文件系统,因为磁盘的分区和引脚分区不一样,为了实现上层统一化),根据你的设备名比如pin4去到内核的驱动链表,根据其主设备号与次设备号找到相关驱动函数。 -
调用驱动函数里面的open,这个open就是对寄存器的操作,从而设置IO口引脚电平。这件事对于单片机来说特变容易,就两句话搞定
sbit pin4 = P1^4; pin4 = 1;
(对应下图的粉色笔迹)
四、shell
shell(壳)是一个特殊的应用,也经常被称为命 令行 。可以理解为是一个命令解释器
例如:当我们输入“ ls -l
”的时候,它将此字符串解释为:
- 在默认路径找到该文件(/bin/ls),
- 执行该文件,并附带参数"-l"。
一个shell对应一个终端 (terminal)。曾经来说,终端是一个硬件设备,用来输入并显示输出。如今,由于图形化界面的普及,终端往往就像下图一样,是一个图形化的窗口。
可以通过这个窗口输入或者输出文本,这个文本直接传递给shell进行分析解释,然后执行,本质就是提供和内核交互的程序。
shell脚本
在没有图形界面之前,shell充当了用户的界面,当用户要运行某些应用时,通过shell输入命令,来运行程序。shell是可编程的,它可以执行符合shell语法的文本,这样的文本叫做shell脚本(script)。
以下是一些关于 Shell 脚本的基本信息:
1. 脚本文件格式: Shell 脚本通常以 .sh
为扩展名,但这不是必须的。脚本文件的第一行通常包含指定 Shell 解释器的路径,例如 #!/bin/bash
表示使用 Bash 解释器执行脚本。
2. 权限设置: 在执行 Shell 脚本之前,需要为脚本文件添加执行权限。可以使用 chmod +x script.sh 命令为脚本添加执行权限。
3. 语法: Shell 脚本遵循 Shell 的语法规则,包括变量、条件语句、循环、函数等。常见的 Shell 脚本语言包括 Bash、sh、zsh 等。
4. 变量: 定义和使用变量,例如:
# 定义变量
name="xiaoming"
# 使用变量
echo "Hello $name!"
5. 选择语句:
# 定义变量
name="xiaoming"
# 条件语句
if [ "$name" == "xiaoming" ]; then
echo "It's xiaoming"
else
echo "It's not xiaoming"
fi
6. 循环:
# 循环
for i in {1..5}; do
echo "Count: $i"
done
7. 函数:
# 定义函数
func(){
echo "Hello from the function"
}
#调用函数
func
8. 命令替换: Shell 脚本支持使用反引号 或 $()
将命令的输出嵌入到脚本中。
# 命令替换
pwd_cmd=`pwd`
echo "current location: $pwd_cmd"
9. 输入输出重定向: 可以使用 <、>、>>
等运算符实现输入和输出的重定向。
# 输出重定向
echo "Hello, World!" > output.txt
# 输入重定向
cat < input.txt