Linux系统的组成部分
- 内核
- shell
- 文件系统
- 应用程序
用户空间与内核通信方式
- 系统调用:请求执行特定操作
- 文件接口:读写文件传递数据和命令
- procfs:读写
/proc文件系统特定文件 - sysfs:读写
/sys文件系统中的文件 - netlink套接字:双向通信用于网络配置、管理
- 共享内存:映射共享内存区域读写数据
- 设备文件:打开和读写设备文件
用户态和内核态的区别及区分原因?
内核态(kernel mode):CPU可以执行所有指令和访问硬件资源,具有更高权限,主要用于操作系统内核运行。
用户态(user mode):CPU只能执行部分指令集,无法直接访问硬件资源,主要运行应用程序。
划分用户态和内核态有助于保证操作系统的安全性、稳定性和易维护性。
- 安全性:通过对权限的划分,用户程序无法直接访问硬件资源,从而避免了恶意程序对系统资源的破坏。
- 稳定性:用户态程序出现问题时,不会影响到整个系统,避免了程序故障导致系统崩溃的风险。
- 隔离性:划分内核和应用程序,有利于系统模块化和维护。
用户空间和内核通信的方式
通过系统调用、异常和设备中断进入内核态。
| 通信方式 | 描述 | 适用场景 |
|---|---|---|
| 系统调用 | 用户请求内核服务 | 通用场景 |
| 驱动程序 | 访问硬件设备 | 设备驱动开发 |
共享内存 mmap | 直接数据交换 | 实时性要求高的场景 |
| 数据拷贝函数 | copy_to_user() 和 copy_from_user() | 数据交换场景 |
/proc 文件系统 | 访问内核数据结构 | 内核状态查看与配置 |
sysctl 接口 | 动态调整内核参数 | 内核参数调整 |
sysfs 文件系统 | 查看和配置内核对象 | 设备树信息查看 |
netlink 套接口 | 网络子系统通信 | 网络通信 |
Linux内核的组成
- 进程管理:创建、管理和调度进程。
- 内存管理:分配与释放物理内存,管理虚拟内存映射。
- 文件系统:提供对存储设备和文件的访问接口。
- 设备驱动:提供对硬件设备的抽象和控制接口。
- 网络协议栈:实现网络协议,提供网络通信功能。
- 系统调用:提供用户空间程序与内核交互的接口。
系统调用与普通函数调用的区别
| 特性 | 系统调用 | 普通函数调用 |
|---|---|---|
| 指令使用 | 使用INT和IRET指令 | 使用CALL和RET指令 |
| 堆栈切换 | 存在,从用户态切换到内核态 | 无切换 |
| 特权指令 | 可以,因此能操控设备 | 不能,受限于用户态 |
| 移植性 | 依赖于内核,不保证移植性 | 平台移植性好 |
| 上下文切换 | 用户空间和内核空间间切换,开销较大 | 仅在用户空间内,开销较小 |
| 操作系统角色 | 操作系统的入口点 | 普通功能函数的调用 |
系统调用的作用
- 提供资源访问:允许应用程序访问操作系统资源,如文件、网络和设备。
- 实现用户态与内核态切换:通过系统调用切换到内核态执行特权操作。
- 提供操作系统服务:封装操作系统服务,如进程管理、内存管理等。
- 实现进程间通信:提供进程间通信机制,如消息传递、共享内存等。
系统调用read/write的内核处理流程
在Linux系统中,系统调用通过软中断实现。open函数在用户空间调用时,会通过C库提供的接口,最终转化为系统调用,由内核中的sys_open函数处理。
- 发起调用:用户空间发起
read/write调用,并传递参数。 - 内核处理:根据调用号找到对应的内核函数处理。
- 文件操作:根据文件描述符找到文件对象,执行读/写。
- 数据传输:读取时从文件或设备读数据到内核空间,写入时从用户空间拷贝数据到内核空间,再写入文件或设备。
- 缓存管理:通过缓存管理、块设备管理等处理数据。
- 返回结果:处理完毕后将结果返回用户空间。
系统调用参数传递
- 系统调用号:通常通过寄存器传递,在ARM架构中,可以通过
swi(svc)指令后的参数指定,,或者在EABI规范中,由r7寄存器进行传递。 - 寄存器传递:系统调用的参数通过寄存器传递。在
ARM架构中,系统调用最多支持6个参数,这些参数分别保存在r0到r5寄存器中。 - 栈传递:如果系统调用的参数超过
6个,或者需要传递结构体等复杂数据类型,那么剩余的参数或数据将通过栈传递。在这种情况下,参数会被依次存放在一块连续的内存区域里,同时在寄存器ebx中保存指向该内存区域的指针(即:该连续内存块的首地址)。
Linux内核同步方式总结
- 中断屏蔽:屏蔽中断,防止竞态条件。
- 原子操作:保证操作的不可中断性。
- 自旋锁:循环等待直至获得锁。
- 读写自旋锁:允许多读一写。
- 顺序锁:适用于读多写少场景。
- 信号量:控制线程访问共享资源的数量。
- 读写信号量:允许多读一写。
- BKL(大内核锁):保护整个内核,但可能导致性能下降。
- Seq锁:适用于并发读和单写场景。
Linux系统中调试崩溃问题的流程
- 收集崩溃信息:包括核心转储文件和错误日志
- 分析核心转储文件:使用调试器(如
GDB)加载核心转储文件 - 查看系统日志:检查
/var/log目录下的日志文件 - 重现崩溃:尽量复现崩溃以观察现象
- 使用调试器:附加到崩溃进程或执行崩溃程序
- 跟踪代码执行流:单步调试、观察变量值、堆栈跟踪
系统调用
- 参考:知乎黄导
EABI和OABI
ABI 是应用程序二进制接口,每个操作系统都会为运行在该系统下的应用程序提供应用程序二进制接口(Application Binary Interface,ABI)。
OABI 和 EABI 最大的区别在于,OABI 的系统调用指令需要传递参数来指定系统调用号,而 EABI 中将系统调用号保存在 r7 中。
系统调用的初始化
系统调用由内核中一个静态数组sys_call_table定义。宏NR_syscalls指定系统调用的数量,默认值400。
系统调用定义在entry-common.S中:
syscall_table_start sys_call_table
...
#include <calls-eabi.S>
...
syscall_table_end sys_call_table系统调用的产生
由用户空间产生,实际发起系统调用的真正工作封装在glibc库中。
内核中系统调用的处理
svc 指令实际上是一条软件中断指令,也是从用户空间主动到内核空间的唯一通路(被动可以通过中断、其它异常) 相对应的处理器模式为从 user 模式到 svc 模式。
svc 指令执行系统调用的大致流程为:
- 执行 svc 指令:产生软中断,跳转到系统中断向量表的
svc向量处执行指令,这个地址是0xffff0008处(也可配置在 0x00000008处),并将处理器模式设置为svc。 - 保存用户上下文:保存用户模式下的程序断点信息,以便系统调用返回时可以恢复用户进程的执行。
- 根据系统调用号(r7)执行相应系统调用:根据
sys_call_table中的定义确定内核中执行的系统调用。 - 返回用户程序:执行完成系统调用后返回用户进程。