1. 系统启动
Linux 系统启动分为三个主要阶段:Bootloader、内核 和 用户空间初始化。
1.1 Bootloader 阶段
- 作用:加载操作系统内核并将控制权移交给内核。
- 过程:
- BIOS/UEFI 初始化硬件并找到启动设备。
- Bootloader(如 GRUB)加载 Linux 内核和初始文件系统(initramfs)。
- 向内核传递启动参数。
1.2 内核阶段
- 作用:初始化硬件并启动系统的核心功能。
- 过程:
- 解压内核并启动硬件检测。
- 加载设备驱动并挂载临时根文件系统(initramfs)。
- 启动第一个用户空间进程(
/sbin/init
或 Systemd)。
1.3 用户空间初始化
- 作用:加载服务并提供用户交互环境。
- 过程:
- 启动初始化系统(Systemd 或 SysVinit)。
- 加载系统服务(如网络、日志、图形界面)。
- 提供用户登录界面,进入工作环境。
1.4 程序运行
- 作用:用户可以运行自己的程序或使用系统应用。
- 过程:
- 用户登录后,Shell(如 Bash)作为命令解释器加载。
- 用户可以运行程序(如 vim、python 或 GUI 应用)。
- 程序通过系统调用与内核交互完成任务。
2. 万物皆文件的概念
“万物皆文件” 是 Linux 系统的核心思想之一。
它的含义是:在 Linux 中,几乎所有东西都可以抽象为文件,这包括硬件设备、进程、网络接口等。通过统一的文件接口,Linux 提供了一种简单而高效的方式与系统交互。
2.1 什么是“万物皆文件”?
在 Linux 系统中,以下几类实体都被当作文件对待:
- 普通文件:常见的文本文件或二进制文件。
- 目录:目录本质上是特殊的文件,用于保存文件的名称和位置。
- 设备文件:硬件设备(如硬盘、显示器、键盘等)在
/dev
中表示为文件。 - 管道和套接字:用于进程间通信的特殊文件。
- 内核和进程信息:通过虚拟文件系统
/proc
和/sys
,可以以文件的形式访问内核和进程的状态。
2.2 文件的种类
在 Linux 中,不同类型的“文件”代表不同的资源:
- 普通文件:
- 如
/etc/passwd
、/var/log/syslog
。 - 存储数据的普通文件,可以直接读取或写入。
- 如
- 目录:
- 如
/home/username
、/var/log
。 - 存储其他文件的列表。
- 如
- 设备文件:
- 位于
/dev
目录下,表示硬件设备。 - 如
/dev/sda
(磁盘设备)、/dev/tty
(终端)。 - 分为两类:
- 块设备(如硬盘)。
- 字符设备(如键盘、鼠标)。
- 位于
- 虚拟文件:
- 位于
/proc
和/sys
,由内核动态生成。 - 如
/proc/cpuinfo
(CPU 信息)、/sys/class/net/
(网络接口)。
- 位于
- 管道和套接字:
- 进程间通信的文件接口。
- 如:命令之间的管道
|
。
2.3 统一文件接口的优势
Linux 系统提供了一种统一的方式对待所有资源:
- 简单性:
- 不需要为每种设备或资源设计单独的接口,所有资源都通过文件的读写操作进行访问。
- 无论是读取文本文件,还是与硬件交互,使用的都是相同的系统调用(如
open
、read
、write
)。
- 扩展性:
- 新增设备或资源时,只需实现与文件相关的接口即可,方便扩展和维护。
- 灵活性:
- 管道和重定向等机制基于文件的抽象提供强大的组合能力。
3. 挂载的概念
挂载(Mount) 是 Linux 文件系统中非常重要的概念。它的作用是将一个设备(如硬盘分区、U 盘、光盘等)的文件系统连接到当前的目录树上,使用户可以通过挂载点访问设备上的内容。
3.1 什么是挂载?
在 Linux 中,硬件设备(如磁盘分区、网络存储)被抽象为文件,而挂载是将设备中的文件系统挂载到某个目录(挂载点)上,从而通过该目录访问设备内容。
- 硬盘分区本身(如
/dev/sda1
)只是一个块设备,无法直接读取文件。 - 挂载会将设备的文件系统映射到一个目录(如
/mnt/data
)。 - 挂载后,用户通过目录树(挂载点)访问该设备的内容。
3.2 挂载的过程
-
找到设备文件
- 在 Linux 中,每个硬件设备都会表示为一个设备文件,通常位于
/dev
目录下。 - 例如:
/dev/sda1
:磁盘的第一个分区。/dev/sr0
:光驱设备。
- 在 Linux 中,每个硬件设备都会表示为一个设备文件,通常位于
-
挂载设备到挂载点
- 将设备挂载到某个目录(挂载点)上,例如
/mnt/data
。 - 挂载后,访问
/mnt/data
就相当于访问设备上的文件系统。
- 将设备挂载到某个目录(挂载点)上,例如
-
使用挂载命令
- 使用
mount
命令进行挂载:sudo mount /dev/sda1 /mnt/data
- 挂载后,
/mnt/data
目录的内容就会显示为/dev/sda1
分区上的文件。
- 使用
-
卸载设备
- 卸载时,使用
umount
命令:sudo umount /mnt/data
- 卸载时,使用
3.3 自动挂载
Linux 系统启动时会根据 /etc/fstab
文件中的配置自动挂载分区和设备。
/etc/fstab
文件示例
/dev/sda1 / ext4 defaults 0 1
/dev/sdb1 /mnt/data ext4 defaults 0 2
- 每行表示一个设备及其挂载信息,系统启动时会根据此文件自动挂载设备。
3.4 挂载点的特点
- 挂载点是一个目录,可以是空目录,也可以是已有内容的目录。
- 挂载后,挂载点原有内容会被隐藏,直到设备被卸载。
- 示例:
mkdir /mnt/test echo "original content" > /mnt/test/file.txt sudo mount /dev/sda1 /mnt/test ls /mnt/test # 显示设备上的文件,原内容被隐藏 sudo umount /mnt/test ls /mnt/test # 恢复原内容
- 示例:
3.5 挂载的用途
- 访问存储设备
- 将硬盘分区、U 盘等挂载到文件系统中,方便访问。
- 网络存储挂载
- 通过挂载 NFS、CIFS 等协议访问网络文件系统。
- 设备挂载
- 挂载光盘、ISO 镜像等设备。
- 特殊文件系统
- 挂载虚拟文件系统(如
/proc
、/sys
)以访问内核和硬件信息。
- 挂载虚拟文件系统(如
3.6 挂载的本质
Linux 的挂载是将设备文件中的文件系统与目录树的某一目录绑定:
- 设备文件(如
/dev/sda1
)只提供硬件的接口。 - 挂载操作将设备的文件系统解析,并通过挂载点暴露文件系统的内容。
4. 文件系统结构
/
(根目录)
- 作用:
- 文件系统的起点,所有文件和目录的顶层。
- 包含操作系统启动和运行所需的最基本内容。
- 包含的核心目录:
/bin
,/boot
,/dev
,/etc
,/home
,/lib
,/usr
等。
/bin
(基础命令)
-
作用:
- 存放基础用户命令的二进制文件。
- 无论系统是否启动为单用户模式,
/bin
中的命令都必须可用。
-
示例命令:
ls
:列出目录内容。cp
:复制文件。cat
:查看文件内容。
-
特点:
- 在 Ubuntu 22 中,
/bin
是/usr/bin
的符号链接:/bin -> /usr/bin
- 在 Ubuntu 22 中,
/sbin
(系统管理命令)
-
作用:
- 存放系统管理工具的二进制文件(仅超级用户可用)。
- 包含用于系统启动、修复和恢复的命令。
-
示例命令:
fsck
:文件系统检查。reboot
:重启系统。
-
特点:
- 在 Ubuntu 22 中,
/sbin
是/usr/sbin
的符号链接:/sbin -> /usr/sbin
- 在 Ubuntu 22 中,
/boot
(启动引导文件)
-
作用:
- 存放系统启动所需的静态文件,如内核、引导程序配置。
-
常见文件:
/boot/vmlinuz
:内核文件。/boot/initrd.img
:内核启动时的临时根文件系统。/boot/grub/
:GRUB 引导加载器的配置文件。
/dev
(设备文件)
-
作用:
-
存放所有硬件设备的文件接口。
-
硬件设备被抽象为文件,通过
/dev
访问。
-
- 常见设备:
/dev/sda
:第一块硬盘。/dev/null
:空设备,丢弃数据。/dev/tty
:终端设备。
/etc
(配置文件)
-
作用:
- 存放系统和应用程序的配置文件。
-
常见文件和目录:
/etc/fstab
:定义文件系统挂载。/etc/passwd
:用户账户信息。/etc/ssh/sshd_config
:SSH 服务配置。/etc/network/interfaces
:网络配置文件(较新版本已由netplan
替代)。
-
特点:
- 所有文件通常是可读的,但只有超级用户才能修改。
/home
(用户主目录)
- 作用:
- 存放普通用户的数据文件,每个用户都有自己的子目录。
-
目录结构:
/home/username
:普通用户的主目录。
- 示例:
/home/alice/
:用户alice
的个人数据存储位置。
/root
(超级用户的主目录)
-
作用:
root
用户的主目录,与普通用户的/home/username
类似。
-
特点:
- 仅供超级用户访问,用于管理整个系统。
/usr
(用户空间的程序和库文件)
-
作用:
-
存放系统用户空间的命令、库文件和共享资源。
-
是系统文件层次中最大的部分。
-
- 子目录:
/usr/bin
:普通用户的命令(如vim
,python
)。/usr/sbin
:系统管理命令(如nginx
,apache2
)。/usr/lib
:库文件。/usr/share
:共享文件(如文档、手册页、图标)。/usr/local
:用户自行安装的软件。
/var
(可变数据)
-
作用:
- 存放经常变化的数据,如日志、队列、缓存等。
-
子目录:
/var/log
:存放日志文件。- 如
/var/log/syslog
:系统日志。 /var/cache
:应用程序缓存数据。/var/tmp
:长期存储的临时文件。/var/spool
:队列数据(如打印任务队列)。
/tmp
(临时文件)
-
作用:
-
存放系统和应用程序运行时的临时文件。
-
系统重启后,
/tmp
中的内容会被清空。
-
/opt
(可选软件包)
-
作用:
- 存放第三方或独立的软件包。
-
示例:
/opt/google/chrome
:谷歌浏览器。/opt/myapp/
:用户自行安装的软件。
/media
(可移动设备挂载点)
-
作用:
- 自动挂载可移动设备(如 USB 驱动器、光盘)。
-
示例:
/media/alice/USB_DRIVE
:用户alice
的 U 盘挂载点。
/mnt
(临时挂载点)
-
作用:
- 手动挂载文件系统的临时目录。
-
用途:
- 管理员临时挂载额外设备(如网络文件系统、硬盘分区)。
/proc
(内核和进程信息)
-
作用:
-
虚拟文件系统,提供内核和进程的运行时信息。
-
文件由内核动态生成,不占用实际存储空间。
-
- 示例:
/proc/cpuinfo
:查看 CPU 信息。/proc/meminfo
:查看内存信息。
/sys
(设备和内核信息)
-
作用:
- 提供硬件设备和内核信息的虚拟文件系统。
-
用途:
- 查看和管理设备参数。
-
示例:
/sys/class/net/
:查看网络设备信息。
/run
(运行时数据)
-
作用:
-
存放系统和应用程序运行时的临时数据。
-
系统启动时会自动创建,重启后会清空。
-
- 常见目录:
/run/lock/
:锁文件。/run/user/
:用户的运行时数据。
/snap
(Snap 包管理器)
-
作用:
- 存放由 Snap 包管理器安装的应用程序。
-
目录结构:
/snap/<应用名>/<版本>
:每个 Snap 应用都有独立的目录。
-
特点:
- 提供应用隔离环境。
4.19. /lib
和 /lib64
(共享库文件)
- 作用:
- 存放系统运行所需的共享库文件(类似于 Windows 的
.dll
文件)。
- 存放系统运行所需的共享库文件(类似于 Windows 的
- 子目录:
/lib
:32 位或通用库文件。/lib64
:64 位库文件。
- 常见文件:
/lib/libc.so.6
:C 标准库。/lib/libpthread.so.0
:POSIX 线程库。
4.20. /srv
(服务数据)
- 作用:
- 存放由系统服务(如 Web 服务器、FTP)提供的数据。
- 示例:
/srv/www
:Web 服务器的数据目录。
4.21. /cdrom
- 作用:
- 一些 Ubuntu 系统中用于挂载光盘的目录。
- 在现代 Ubuntu 中不常使用,通常使用
/media
替代。
目录汇总表
目录 | 作用 | 特点或常见内容 |
---|---|---|
/ |
根目录,文件系统的起点。 | 包含所有其他目录,系统启动的基础。 |
/bin |
基本用户命令(如 ls , cp )。 |
在 Ubuntu 22 中,/bin 是 /usr/bin 的符号链接。 |
/sbin |
系统管理命令(如 fsck , reboot )。 |
在 Ubuntu 22 中,/sbin 是 /usr/sbin 的符号链接。 |
/boot |
存放系统启动引导相关文件。 | 包括内核文件(vmlinuz )、启动加载器配置(grub/ )。 |
/dev |
存放设备文件,抽象硬件设备(如硬盘、终端、光驱等)。 | 设备以文件形式存在,如 /dev/sda (硬盘),/dev/null (空设备)。 |
/etc |
系统和应用程序的配置文件。 | 包括 /etc/fstab (文件系统挂载)、/etc/hosts (主机名解析)、/etc/ssh/ (SSH 配置)。 |
/home |
普通用户的主目录,存放用户数据和配置。 | 每个用户的主目录以用户名命名,如 /home/alice 。 |
/root |
超级用户的主目录。 | 与普通用户的 /home 类似,但位于根目录下,仅供 root 用户使用。 |
/usr |
用户空间的程序和库文件。 | 包括 /usr/bin (用户命令)、/usr/lib (库文件)、/usr/share (共享资源)、/usr/local (用户手动安装的软件)。 |
/var |
可变数据(如日志、缓存、队列)。 | 包括 /var/log (日志文件)、/var/cache (缓存数据)、/var/tmp (临时文件)。 |
/tmp |
临时文件存储目录。 | 系统和应用运行时的临时文件,通常在系统重启后清空。 |
/opt |
第三方软件包存储目录。 | 手动安装的软件通常存放在这里,如 /opt/google/chrome 。 |
/media |
自动挂载的可移动设备目录。 | 可移动设备(如 U 盘、光盘)会自动挂载到 /media/username/设备名 。 |
/mnt |
手动挂载文件系统的临时目录。 | 系统管理员手动挂载设备的临时位置,如挂载外部硬盘、网络文件系统等。 |
/proc |
内核和进程信息的虚拟文件系统。 | 包括 /proc/cpuinfo (CPU 信息)、/proc/meminfo (内存信息)、/proc/[PID] (特定进程的状态信息)。 |
/sys |
提供硬件设备和内核信息的虚拟文件系统。 | 包括 /sys/class/ (硬件设备信息,如网络设备)。 |
/run |
存放运行时的临时数据。 | 包括 /run/lock (锁文件)、/run/user/ (用户会话信息)。 |
/snap |
Snap 包管理器的应用程序存储目录。 | 每个 Snap 应用程序以独立的目录存放,如 /snap/chromium/2000 。 |
/lib |
系统运行时的共享库文件。 | 包括 C 标准库(libc.so )、线程库(libpthread.so ),支持系统和应用程序运行。 |
/lib64 |
64 位共享库文件。 | 提供 64 位程序的动态库支持,类似于 /lib 。 |
/srv |
系统服务(如 Web 服务器、FTP)的数据目录。 | 包括 /srv/www (Web 服务器文件目录)。 |
/cdrom |
某些系统用于挂载光盘的目录(现代 Ubuntu 不常用)。 | 通常由管理员手动挂载,现代系统多使用 /media 替代。 |
5. 环境变量
在 Linux 系统中,环境变量(Environment Variables) 是一个重要的概念,它存储了影响 Shell 和应用程序行为的参数。例如,PATH 变量决定了系统如何查找可执行文件,HOME 变量指定了用户的主目录,USER 变量指定了用户名。
如:
HOME=/home/user
USER=user
PATH=/usr/local/bin:/usr/bin:/bin
5.1. 查看环境变量
- 查看所有环境变量:
env
- 查看单个变量:
echo $PATH printenv HOME
- 查看所有变量(包括本地变量):
set | less
5.2. 修改环境变量
临时修改(仅当前终端有效)
-
使用
export
:export MY_VAR="Hello" echo $MY_VAR # 输出 Hello
关闭终端后失效。
-
删除变量:
unset MY_VAR
永久修改(影响所有终端)
- 用户级(仅影响当前用户)
配置文件 | 适用 Shell | 作用 |
---|---|---|
~/.bashrc |
Bash 交互式终端 | 每次打开终端都会加载 |
~/.profile |
登录 Shell(GUI & SSH) | 每次用户登录都会加载 |
示例(添加环境变量):
echo 'export PATH=$PATH:/home/user/bin' >> ~/.bashrc
source ~/.bashrc
- 系统级(影响所有用户)
配置文件 | 作用 |
---|---|
/etc/environment |
全局环境变量(不支持 export ) |
/etc/profile |
适用于所有用户的 登录 Shell |
/etc/bash.bashrc |
适用于所有用户的 Bash 交互式 Shell |
5.3. 相关命令
命令 | 作用 |
---|---|
echo $VAR |
查看变量的值 |
env |
列出当前环境变量 |
export VAR=value |
设置环境变量(当前会话) |
unset VAR |
删除变量 |
source 文件 |
使修改的配置文件立即生效 |
5.4. 总结
方式 | 影响范围 | 适用文件 |
---|---|---|
临时修改 | 当前终端 | export VAR=value |
永久修改(当前用户) | 影响终端 | ~/.bashrc (终端)~/.profile (GUI+终端) |
永久修改(所有用户) | 全局生效 | /etc/environment (全局变量)/etc/profile (登录 Shell) |
6. Vi 的使用
插入模式命令
i
:将光标前进入插入模式。a
:将光标后进入插入模式。I
:将光标移到当前行的行首进入插入模式。A
:将光标移到当前行的行尾进入插入模式。o
:在当前行下方插入新行,并进入插入模式。O
:在当前行上方插入新行,并进入插入模式。
文件操作命令
:w
:保存当前文件。:q
:退出vi
。:wq
、:x
:保存文件并退出vi
。:q!
:强制退出vi
,不保存文件。:e <file>
:打开并编辑指定的文件。
删除命令
x
:删除光标所在的字符。dw
:删除光标所在字符至下一个单词的起始处。dd
:删除光标所在的整行。ndd
:删除光标所在行及其后面的 n-1 行(n 为数字)。
复制/粘贴命令
yy
:复制光标所在的整行。nyy
:复制当前行及其后的 n-1 行(n 为数字)。p
:粘贴(在光标后粘贴已复制或已删除的内容)。
查找与替换命令
/pattern
:从光标开始向文件尾搜索pattern
,后按n
(向下查找)或N
(向上查找)。:%s/p1/p2/g
:将文件中所有的p1
替换为p2
。:%s/p1/p2/gc
:替换时需要确认。
行操作命令
dd
:删除光标所在的整行。yy
:复制光标所在的整行。p
:在光标后粘贴已复制或已删除的内容。u
:撤销上一个操作。Ctrl-r
:重做撤销的操作。
翻页与滚动命令
Ctrl-f
:向前翻一页。Ctrl-b
:向后翻一页。Ctrl-d
:向下滚动半页。Ctrl-u
:向上滚动半页。zz
:将当前行居中显示。
行号与显示命令
:set number
:显示绝对行号。:set relativenumber
:显示相对行号。:set nonumber
:禁用行号显示。
Vim 配置与帮助命令
:set
:查看或设置vi
的配置选项。:help
:查看帮助文档(通常是vim
中使用,vi
中没有这个命令,但某些vi
版本可能有帮助命令)。
其他命令
:normal
:执行普通模式命令(一般不在vi
中常用,但某些实现可以支持)。:help
:查看帮助文档。
7. GCC + Makefile 的使用
在软件开发过程中,GCC(GNU Compiler Collection) 是一个广泛使用的编译器,它支持多种编程语言,特别是在 C 语言和 C++ 中的应用十分广泛。结合使用 Makefile,可以高效地管理和自动化编译过程。
7.1. GCC 指令的使用
GCC 指令主要用于编译 C 或 C++ 源代码文件,常见的用法包括:
简单的 GCC 编译指令
gcc -o test a.c b.c
这条命令会将 a.c
和 b.c
编译并链接成一个可执行文件 test
。-o
指定输出的文件名。
打印依赖
gcc -M c.c // 打印出 c.c 文件的依赖
这条命令会显示 c.c
文件所依赖的头文件,可以了解哪些文件会影响 c.c
文件的编译。
把依赖写入文件
gcc -M -MF c.d c.c // 把依赖写入文件 c.d
这条命令会将 c.c
文件的依赖关系写入到 c.d
文件中,便于管理。
使用 -MD
和 -MF
选项
gcc -c -o c.o c.c -MD -MF c.d // 编译 c.c,生成 c.o,同时把依赖写入 c.d
这条命令会同时生成目标文件和依赖文件,-MD
表示生成 .d
格式的依赖文件。
7.2. Makefile 基础知识
Makefile 是一个自动化的构建工具,通过定义规则来描述文件之间的依赖关系和如何生成目标文件。
7.2.1 目标和依赖
Makefile 的基本结构是:
目标: 依赖1 依赖2 ...
[TAB] 命令
当目标文件不存在或依赖文件更新时,Make 会自动执行命令生成目标文件。
7.2.2 假目标: .PHONY
.PHONY: clean
clean:
rm -f *.o program
.PHONY
用于声明一个目标不是文件,避免与同名的文件发生冲突。通常用于 clean
目标。
7.2.3 变量和条件判断
-
简单变量定义:
A = xxx # A 的值在定义时确定 B := yyy # B 的值在使用时确定 C ?= zzz # C 的值在定义时确定,如果未定义则使用 zzz D += uuu # D 的值在定义时确定,如果已定义则追加 uuu
-
条件判断:
这段Makefile
中的条件判断代码使用了ifeq
语句来根据条件来设置不同的值。在Makefile
中,条件判断是一种非常常见的功能,可以帮助我们根据不同的环境或条件来做出不同的决策,比如选择不同的编译选项。语法:
ifeq (条件1, 条件2) # 如果条件1等于条件2,执行这部分命令 else # 如果条件1不等于条件2,执行这部分命令 endif
在 Makefile
中,ifeq
是条件判断的核心,它会比较两个条件,如果它们相等,则执行 ifeq
后面的命令;如果不相等,则执行 else
后面的命令(如果存在)。
ifeq (条件1, 条件2)
:检查条件1
和条件2
是否相等。else
:如果ifeq
条件不成立,执行else
语句块的内容。-
endif
:结束条件判断语句块。具体示例分析:
ifeq ($(CC), gcc) CFLAGS = -O2 else CFLAGS = -O0 endif
-
条件判断:
ifeq ($(CC), gcc)
:这行代码的意思是检查$(CC)
变量的值是否等于gcc
。$(CC)
变量通常表示编译器的名称,如果使用的是gcc
编译器,那么这个条件成立。
-
执行的动作:
- 如果
$(CC)
的值为gcc
,则执行CFLAGS = -O2
,这意味着编译器将使用-O2
优化级别进行编译。 - 如果
$(CC)
的值不是gcc
,则执行CFLAGS = -O0
,这表示使用-O0
优化级别,即不进行优化。
- 如果
7.2.4 常见的 Makefile 函数
foreach
函数
功能:
foreach
用于遍历列表中的每个元素,并对每个元素执行指定的操作。它类似于循环(迭代)的功能。
语法:
$(foreach var, list, text)
-
var
:变量名,用于表示列表中的每个元素。 -
list
:需要遍历的列表,可以是由空格分隔的多个元素。 -
text
:在每个元素上执行的操作。示例:
FILES = a.c b.c c.c OBJ_FILES = $(foreach file, $(FILES), $(file:.c=.o)) $(info OBJ_FILES = $(OBJ_FILES)) # 输出替换后的目标文件列表
-
上面的例子会将每个
.c
文件替换为对应的.o
文件,输出:OBJ_FILES = a.o b.o c.o
应用场景:
-
遍历文件列表,对每个文件执行相同的操作(如替换扩展名、编译等)。
-
批量处理变量中的元素。
filter
函数
功能:
filter
用于从一个文本(字符串)中筛选出符合模式的部分。可以用它从列表中提取出符合某种模式的项。
语法:
$(filter pattern..., text)
pattern...
:一个或多个模式,可以是通配符(例如*.c
)。-
text
:需要筛选的文本,通常是由空格分隔的多个元素。示例:
FILES = main.c util.c test.h header.h demo.c C_SOURCES = $(filter %.c, $(FILES)) $(info C_SOURCES = $(C_SOURCES)) # 输出筛选出的 .c 文件
-
输出:
C_SOURCES = main.c util.c demo.c
应用场景:
- 从文件列表中筛选出特定类型的文件,例如筛选
.c
文件、.h
文件等。 - 用于检查某个变量是否符合某个模式。
wildcard
函数
功能:
wildcard
用于根据给定的模式获取匹配的文件列表。它类似于 shell 中的通配符功能。
语法:
$(wildcard pattern)
-
pattern
:匹配的模式,可以是通配符(例如*.c
,src/*.c
等)。示例:
SRC_FILES = $(wildcard src/*.c) $(info SRC_FILES = $(SRC_FILES)) # 输出匹配到的文件列表
-
假设
src
目录下有a.c
和b.c
,那么输出:SRC_FILES = src/a.c src/b.c
应用场景:
- 动态获取符合模式的文件列表。
- 用于获取某个目录下的所有源文件、头文件,或者其他类型的文件。
patsubst
函数
功能:
patsubst
用于将文本中的某个模式替换为另一个模式。它是字符串替换的工具。
语法:
$(patsubst pattern, replacement, $(var))
pattern
:要替换的模式(可以是通配符)。replacement
:替换成的内容。-
var
:需要进行替换操作的字符串。示例:
SRCS = main.c util.c test.c OBJS = $(patsubst %.c, %.o, $(SRCS)) $(info OBJS = $(OBJS)) # 输出替换后的目标文件列表
-
输出:
OBJS = main.o util.o test.o
应用场景:
- 替换文件扩展名。例如,将
.c
文件替换为.o
文件。 - 用于格式化路径或字符串。
realpath
和 abspath
函数
功能:
realpath
:返回一个文件的绝对路径,并且去掉任何相对路径部分(如.
和..
)。-
abspath
:将相对路径转换为绝对路径。示例:
SRCS := $(shell find $(SRC_DIRS) -type f -name '*.c') SRCS := $(realpath $(SRCS))
-
这会将
SRCS
中的相对路径转换为绝对路径。应用场景:
- 确保路径的一致性,将相对路径转换为绝对路径,避免路径格式错误。
$(info ...)
函数
功能:
$(info ...)
用于输出调试信息,打印你想要显示的内容。
示例:
$(info Processing $(SRC_DIRS) for source files...)
-
这会将
Processing . src for source files...
打印到终端。应用场景:
- 打印调试信息,检查变量、路径或任何中间步骤的结果。
总结
函数 | 功能 | 示例 |
---|---|---|
foreach |
遍历列表并执行操作 | $(foreach var, list, text) |
filter |
在文本中筛选符合模式的部分 | $(filter pattern..., text) |
wildcard |
获取符合模式的文件列表 | $(wildcard pattern) |
patsubst |
字符串替换,替换符合模式的部分为替代文本 | $(patsubst pattern, replacement, $(var)) |
realpath |
获取文件的绝对路径 | $(realpath $(SRCS)) |
abspath |
将相对路径转换为绝对路径 | $(abspath $(SRC_DIRS)) |
info |
输出调试信息 | $(info some debug message) |
7.3. Makefile 实例(Linux通用)
# 编译器设置
CC = gcc
CFLAGS = -Wall -Wextra
TARGET = myapp
BUILD_DIR = build
# 源文件配置(确保目录不重叠)
SRC_DIRS := src # 只保留实际需要编译的目录
CFLAGS += $(addprefix -I, $(SRC_DIRS))
# 自动查找源文件(去重处理)
SRCS := $(shell find $(SRC_DIRS) -type f -name '*.c' | sort -u)
# 生成目标文件路径(保留目录结构)
OBJS := $(SRCS:%.c=$(BUILD_DIR)/%.o)
DEPS := $(OBJS:.o=.d)
# 调试输出
$(info Valid Sources: $(SRCS))
$(info Objects: $(OBJS))
# 默认目标
all: $(TARGET)
# 链接规则
$(TARGET): $(OBJS)
$(CC) $^ -o $@ $(LDFLAGS)
# 精确编译规则
$(BUILD_DIR)/%.o: %.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -MMD -MT $@ -c $< -o $@
# 依赖处理
-include $(DEPS)
clean:
rm -rf $(BUILD_DIR) $(TARGET)
.PHONY: all clean
8. 系统调用
在 Linux 操作系统中,用户程序与硬件或内核之间的交互通常通过系统调用(system call) 实现。
8.1. 系统调用基础
系统调用是用户空间与内核函数(内核提供的底层操作)之间的接口。当用户程序需要执行某些受限制的操作(如硬件访问、文件管理等),它会通过系统调用进入内核执行。内核会在安全的环境下执行这些操作,并将结果返回给用户程序。
8.1.1 用户态与内核态
用户态:应用程序运行的非特权模式,不能直接访问硬件资源
内核态:操作系统运行的特权模式,可直接操作硬件和系统资源
切换机制:通过软中断(int 0x80)或syscall指令触发,CPU切换特权级别并跳转到系统调用入口
8.1.2 系统调用流程
在操作系统的设计中,系统调用是用户程序和操作系统内核之间的接口,允许用户程序请求内核执行某些特权操作,如文件管理、内存分配、进程管理等。
-
用户态到内核态的切换
-
用户程序将参数存入寄存器
- 用户程序通过调用系统调用接口(如
open
、read
、write
)来请求操作系统执行某些任务。系统调用需要参数,这些参数通常包括文件路径、读写缓冲区、文件描述符等。 - 这些参数会存入 CPU 寄存器 中。用户程序将其需要传递给内核的参数放在特定的寄存器中,以便在执行系统调用时内核能够读取。
- 用户程序通过调用系统调用接口(如
-
触发软中断(软中断指令)
- 执行
syscall
(或在某些系统中为int 0x80
)指令时,程序会触发一个 软中断,这是一个软中断(与硬件中断不同)信号,它通知 CPU 需要切换到内核态。 - 软中断通过执行特殊的指令(如
syscall
指令)让 CPU 跳转到内核空间,调用操作系统内核的代码。这一过程是从用户空间到内核空间的切换,进入到特权模式。
- 执行
-
CPU 切换到内核态
- 在触发软中断之后,CPU 会从 用户态 切换到 内核态。这时,程序的控制权不再属于用户空间,而是进入内核空间。
- 内核空间是操作系统的运行环境,它具有访问硬件、管理内存和控制进程的权限。用户程序通过系统调用进入内核空间,操作系统通过这些接口来保证系统的安全性和稳定性。
-
跳转到系统调用处理程序
- 当 CPU 进入内核态时,它会跳转到相应的 系统调用处理程序。不同的系统调用会触发不同的处理程序。
- 这些处理程序会根据传入的参数执行实际的操作。例如,
open
系统调用的处理程序会打开一个文件,read
系统调用会从文件中读取数据,write
系统调用会向文件写入数据。
-
-
内核态到用户态的切换
-
内核验证参数并执行操作
- 一旦 CPU 进入内核空间,内核会对传入的参数进行验证(例如,检查文件路径的有效性、验证文件的访问权限等)。
- 内核执行完相关的操作后,它将结果存储在适当的位置(例如返回值、错误码等)。在文件 I/O 系统调用中,操作系统会操作文件系统、读写磁盘,并更新文件描述符表。
-
将结果返回给用户程序
- 内核完成操作后,它会将结果返回给用户程序。这些结果通常存储在寄存器中,并通过系统调用的返回值传递给用户空间。
- 例如,在
read
系统调用中,返回值通常是成功读取的字节数,或者在发生错误时返回-1
。
-
切换回用户态
- 在操作完成后,操作系统会通过 返回指令(例如
ret
)切换回用户态。CPU 会恢复到原本的用户空间执行环境。 - 用户程序继续执行,可以使用从内核返回的数据进行进一步处理。
- 在操作完成后,操作系统会通过 返回指令(例如
-
用户程序调用open() → 触发0x80中断 → CPU切换内核态 → 执行sys_open()
↓ ↑
参数存入寄存器 → 内核验证参数 → 文件操作 → 结果返回寄存器 → 切换回用户态
8.2 分类与核心系统调用
8.2.1 进程管理
作用:创建、终止、调度进程,控制进程行为。
核心系统调用:
-
fork()
:创建子进程,返回子进程 PID(父进程中)或 0(子进程中)。 -
execve()
:加载并执行新程序,替换当前进程映像。 -
exit()
:终止当前进程,释放资源。 -
wait()
/waitpid()
:等待子进程终止并回收资源。 -
getpid()
/getppid()
:获取当前进程及其父进程的 PID。 -
nice()
:调整进程优先级。 -
kill()
:向指定进程发送信号(如SIGTERM
终止进程)。
8.2.2 文件操作
作用:文件/目录的创建、读写、属性管理。
核心系统调用:
-
open()
:打开或创建文件,返回文件描述符。 -
read()
/write()
:读写文件内容。 -
close()
:关闭文件描述符。 -
lseek()
:移动文件读写指针。 -
stat()
/fstat()
:获取文件元数据(如大小、权限)。 -
mkdir()
/unlink()
:创建目录或删除文件。 -
mmap()
:将文件映射到内存,实现高效读写。 -
ioctl()
:设备文件的控制命令(如调整终端参数)。
8.2.3 内存管理
作用:分配、释放内存,管理虚拟内存空间。
核心系统调用:
-
brk()
/sbrk()
:调整堆内存的末尾地址。 -
mmap()
/munmap()
:映射文件或匿名内存到进程地址空间。 -
mprotect()
:修改内存区域的保护属性(如只读、可执行)。 -
mlock()
:锁定内存页,防止被交换到磁盘。
8.2.4 进程间通信(IPC)
作用:实现进程间数据交换与同步。
核心系统调用:
-
管道:
pipe()
创建无名管道。 -
信号:
signal()
注册信号处理函数,kill()
发送信号。 -
共享内存:
shmget()
/shmat()
创建和附加共享内存。 -
消息队列:
msgget()
/msgsnd()
创建和发送消息。 -
信号量:
semget()
/semop()
控制同步信号量。 -
Socket:
socket()
/bind()
/send()
网络或本地进程通信。
8.2.5 系统信息与配置
作用:获取系统信息或修改配置。
核心系统调用:
-
uname()
:获取系统名称、版本、硬件信息。 -
sysinfo()
:查询系统内存、进程数等资源状态。 -
gettimeofday()
:获取当前时间(微秒级精度)。 -
sysctl()
:动态修改内核参数(如网络配置)。
8.2.6 权限与安全
作用:管理用户权限、文件访问控制。
核心系统调用:
-
chmod()
:修改文件权限(如chmod 755 file
)。 -
chown()
:修改文件所有者。 -
setuid()
/setgid()
:切换进程的有效用户/组 ID。 -
capset()
:设置进程的能力(Capabilities)。
8.2.7 网络通信
作用:实现网络数据传输。
核心系统调用:
-
socket()
:创建套接字(TCP/UDP)。 -
bind()
/listen()
/accept()
:服务端绑定端口并监听连接。 -
connect()
:客户端连接服务端。 -
send()
/recv()
:发送/接收数据。 -
getsockopt()
/setsockopt()
:配置套接字参数。
8.3 总结
类别 | 核心系统调用 | 典型应用场景 |
---|---|---|
进程管理 | fork , execve , waitpid , kill |
多进程程序、守护进程 |
文件操作 | open , read , mmap , ioctl |
文件读写、设备控制 |
内存管理 | brk , mmap , mlock |
动态内存分配、内存映射文件 |
进程间通信 | pipe , shmget , msgget , semop |
进程间数据共享、同步 |
系统信息 | uname , sysinfo , gettimeofday |
系统监控、日志记录 |
权限管理 | chmod , setuid , capset |
提升/降低权限、文件保护 |
网络通信 | socket , bind , send , recv |
网络服务端/客户端开发 |
系统调用是用户程序和内核交互的桥梁,它提供了用户空间程序与硬件、内核资源进行交互的接口。通过系统调用,用户程序可以方便地完成文件操作、网络通信、内存管理、进程管理等任务,而不需要直接访问底层硬件和操作系统内部实现。
9. Linux 用户程序开发浅析
Linux 用户程序开发是一个分层的过程,每一层都有其特定的作用,彼此协作以实现高效、稳定的系统操作。
9.1 硬件层(Physical Hardware Layer)
硬件层是操作系统和用户程序的基础,包含了计算机所有的物理设备,提供计算和数据存储的功能。
9.1.1 物理设备
- CPU:负责运算和执行程序指令。
- 内存(RAM):用于存储程序代码、数据和缓冲区。
- 存储设备:硬盘(HDD/SSD),存放文件和数据。
- 网络接口卡(NIC):处理网络通信。
- 外设:如键盘、鼠标、显示器等。
9.1.2 硬件抽象
-
操作系统通过 硬件抽象层(HAL) 和 设备驱动程序 将硬件功能抽象为统一的接口。这些驱动程序将硬件的底层操作封装成标准的函数接口(如文件操作接口),使得上层的软件可以通过统一的方式与硬件交互,而无需关心具体硬件的实现细节。
-
设备驱动:每个硬件设备都有对应的设备驱动,设备驱动依赖于特定的总线和协议来与硬件通信,驱动通过为硬件提供标准化的 文件接口(如
/dev/sda
)来管理硬件。用户空间的程序和内核层通过标准系统调用(如open()
、read()
、write()
)与驱动程序进行交互,进而访问硬件。 -
总线和协议:通过不同的总线(如 PCIe、USB、I2C、SPI)将硬件设备连接到计算机系统。这些总线标准和协议帮助操作系统管理硬件设备的通信和控制,提供一致的访问接口。
-
总线和协议定义了硬件设备如何连接和通信的规则,而协议驱动实现了这些规则的操作,设备驱动则依赖于协议驱动来与特定硬件设备进行通信。
-
-
每个硬件设备都有对应的寄存器地址和控制信号,操作系统通过 内存映射 I/O(MMIO) 或 直接内存访问(DMA) 来与硬件进行交互,这样可以高效地进行数据传输和控制。
-
中断机制:硬件通过 IRQ(中断请求)向操作系统发出信号,内核通过中断处理程序响应硬件事件,如键盘输入、网络数据接收等,确保系统能够及时处理硬件产生的事件。
9.2 内核层(Kernel Space)
内核层是操作系统的核心,直接与硬件交互,并为用户层提供抽象接口。它管理着计算机的所有资源,如 CPU、内存、磁盘、设备等。
9.2.1 核心功能
- 进程调度:内核使用 CFS(完全公平调度器) 来调度和分配 CPU 时间片,确保多进程的公平执行。
- 内存管理:虚拟内存管理,通过 页表 和 内存分页 实现物理内存的虚拟化。
- 文件系统:通过 VFS(虚拟文件系统) 提供对各种文件系统(如 ext4、btrfs)的统一访问。
- 设备驱动管理:通过设备驱动程序与硬件交互,设备抽象为文件接口,提供统一的访问方法。
- 网络协议栈:内核实现 TCP/IP 协议,提供网络通信支持。
9.2.2 设备驱动子系统
负责管理各种硬件设备,并为用户空间程序提供访问这些硬件的标准接口
-
设备类型:
-
字符设备:如串口设备(
/dev/ttyS0
)等按字节访问。 -
块设备:如硬盘(
/dev/sda
)等按块访问数据。 -
网络设备:如网卡,通过套接字接口进行通信。
-
-
驱动实现机制:
- 驱动通过
file_operations
结构体定义标准的文件操作接口(如open()
、read()
、write()
)。 - 使用
register_chrdev()
注册字符设备驱动,ioremap()
将硬件内存映射到内核空间。
- 驱动通过
9.2.3 系统调用接口
- 系统调用是内核和用户空间之间的接口,允许用户程序通过系统调用与内核交互。常见系统调用包括:
open()
,read()
,write()
,ioctl()
等。
- 系统调用通过
syscall
指令触发,进入内核进行处理。
9.3 用户层(User Space)
用户层是用户直接与系统交互的地方,运行各种应用程序,如文本编辑器、浏览器、数据库等。用户程序可以选择通过系统库、第三方库或者直接使用系统调用来实现自己的功能。
9.3.1 系统库(System Libraries)
系统库是提供给用户的函数库,系统函数是系统库中的函数,封装了底层的系统调用,而系统调用是用户空间与内核函数(内核提供的底层操作)之间的接口。
-
glibc:GNU C Library,封装了底层的系统调用,提供如
printf()
,malloc()
,fopen()
等功能,简化开发。 -
数学库(libm):提供数学函数,如
sin()
,cos()
等。 -
线程库(pthread):为多线程程序提供线程创建、同步和通信功能。
9.3.2 第三方库
第三方库通常是对系统库或系统函数的封装,它们通过这些接口提供更高层次、更专业的功能,简化开发者的工作。
-
工具库:
-
OpenCV:用于图像处理和计算机视觉的库,提供图像处理、机器学习等功能。
-
i2ctools:用于通过 I2C 总线与硬件设备通信的工具集。
-
FFmpeg:用于音视频的处理、格式转换、编辑、流媒体推流等功能,支持多种编码格式和平台。
-
libcurl:提供 HTTP 客户端功能,支持文件传输。
-
OpenSSL:提供加密和解密功能。
-
-
算法库:
- YOLO:一种目标检测算法,提供实时的物体识别和定位功能。
- BLAS/LAPACK:用于数值计算和线性代数。
- Boost:提供 C++ 扩展库,提供数据结构、算法等。
- 开发框架:
- ROS2:机器人操作系统,提供机器人控制、通信、传感器接口等功能。
- Qt:用于开发跨平台 GUI 应用。
- OpenMP:支持并行计算,简化多核处理编程。
9.4 跨层交互示例:读取文件
在 Linux 系统中,从用户层到内核层的文件读取操作流程如下:
-
用户层程序调用
fopen("file.txt", "r")
(系统函数)。 -
glibc
将其转换为系统调用open()
,并传递给内核。 -
内核通过 虚拟文件系统(VFS) 查找文件系统类型(如 ext4)。
-
文件系统驱动访问对应的存储设备(如硬盘)。
-
硬盘的控制器通过 DMA(直接内存访问) 将数据传输到内存。
-
数据返回给用户层,程序通过
fread()
(系统函数) 读取数据。
9.5 总结
层次 | 作用 | 典型组件 |
---|---|---|
硬件层 | 物理设备,如 CPU、内存、磁盘 | 硬件芯片、总线接口 |
内核层 | 提供 设备驱动、进程管理、文件系统、网络 等功能 | 设备驱动、文件系统、网络协议栈 |
用户层 | 运行用户程序,调用 第三方库/系统调用 | Shell、GUI 程序、服务进程 |
10. Linux驱动开发浅析
10.1 驱动框架
Linux驱动框架主要有三种形态,分别是传统单一文件驱动、总线设备驱动模型、和设备树驱动模型。每一种驱动模型具有不同的结构和实现方式,它们的优缺点以及在现代嵌入式系统中的应用各不相同。
10.1.1 传统单一文件驱动
特点:
- 在传统的Linux驱动模型中,驱动程序通常是一个单一的文件,这个文件中包含了硬件操作和软件操作的代码逻辑。
- 驱动代码将硬件控制代码(例如寄存器配置、初始化等)和高层软件接口代码(如文件操作)混合在一起。
缺点:
- 代码耦合度高:硬件操作和软件逻辑的紧密耦合使得代码的可维护性差。任何硬件变化都可能需要修改整个驱动。
- 移植性差:由于硬件相关代码和软件相关代码混在一起,不同平台之间的移植性较差。如果硬件平台发生变化,驱动代码的调整和重写工作会非常繁琐。
关键结构体:
file_operations
:这是一个结构体,定义了文件操作接口,例如打开、关闭、读取、写入等操作。通过这个结构体,驱动程序可以与内核中的VFS(虚拟文件系统)进行交互。struct file_operations { int (*open)(struct inode *, struct file *); int (*release)(struct inode *, struct file *); ssize_t (*read)(struct file *, char __user *, size_t, loff_t *); ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *); ... };
应用场景:
- 这种模式适用于简单的设备,硬件和软件的耦合较少,或者在驱动开发初期阶段使用。
10.1.2 总线设备驱动模型
分层结构:
总线设备驱动模型的核心是通过一个分层的结构来实现驱动的拆分,驱动和硬件之间的耦合减少了,增强了驱动的可移植性和可维护性。
- 驱动文件(Platform Driver):负责处理设备的初始化、资源分配、控制接口等,定义了设备的操作接口。实现了
platform_driver
结构体,并注册到内核中。struct platform_driver { int (*probe)(struct platform_device *); int (*remove)(struct platform_device *); struct device_driver driver; };
- 设备文件(Platform Device):表示平台上的具体设备,通过
platform_device
结构体描述硬件设备,并注册到内核中。struct platform_device { struct device dev; struct resource *resource; ... };
- 资源文件(Resource):硬件资源通过
resource
结构体进行描述,如内存地址、中断号、端口等。
优点:
- 驱动与硬件解耦:通过驱动与设备之间的抽象层(如总线)来实现软硬件的解耦。设备的具体硬件资源(例如寄存器地址、IRQ号等)被描述为资源,并通过内核的资源管理机制进行管理。
- 移植性和扩展性好:这种结构能够更好地支持不同硬件平台,只需要编写适配平台的驱动即可。
关键函数:
platform_driver_register()
:将一个platform_driver
注册到内核中,让内核知道该驱动能够处理哪些平台设备。int platform_driver_register(struct platform_driver *driver);
platform_device_register()
:将一个platform_device
注册到内核,表明该设备可由某个驱动管理。int platform_device_register(struct platform_device *pdev);
应用场景:
- 这种模式适用于硬件设备的抽象和标准化平台(如ARM架构的嵌入式系统),能够实现硬件和驱动代码的解耦,使得驱动可以更容易地在不同平台上移植。
10.1.3 设备树驱动模型
核心变化:
- 设备树驱动模型的核心变化是使用设备树(Device Tree)来替代硬编码的硬件资源。设备树文件(通常以
.dts
后缀为扩展名)描述了硬件平台的设备信息,如设备的寄存器地址、IRQ号等。 - 设备树通过
compatible
属性来匹配驱动与硬件设备之间的关系,驱动可以根据设备树中提供的信息进行初始化和配置。
设备树组成:
- 硬件描述:设备树中包含了硬件资源的描述,如设备的寄存器地址、IRQ号、DMA通道等。例如:
/ { uart0: uart@1000 { compatible = "myvendor,my-uart"; reg = <0x1000 0x100>; interrupts = <0x10>; }; };
- 匹配驱动:在驱动中,可以通过
of_match_table
结构体与设备树节点的compatible
属性进行匹配。例如:static const struct of_device_id my_uart_of_match[] = { { .compatible = "myvendor,my-uart", }, { /* sentinel */ }, };
关键操作:
- 解析设备树节点:驱动程序需要解析设备树节点来获取设备的相关信息,例如寄存器地址、IRQ号等。Linux内核提供了
of_
系列函数来访问设备树信息。struct device_node *np = of_find_node_by_name(NULL, "uart0"); if (np) { unsigned int irq = of_get_property(np, "interrupts", NULL); unsigned long addr = of_get_address(np, 0, NULL, NULL); }
- 使用of_系列函数获取资源:通过
of_get_property()
、of_get_address()
、of_irq_to_resource()
等函数,驱动可以获得硬件资源。
应用场景:
- 设备树模型广泛用于现代嵌入式系统中,尤其是ARM平台。它将硬件的描述从驱动程序中分离出来,使得硬件与驱动之间的耦合大大减少,便于不同硬件平台的支持和移植。
10.1.4 总结
- 传统单一文件驱动适合简单、硬件固定的场景,但移植性差,维护困难。
- 总线设备驱动模型通过分层架构使得硬件和软件的解耦更为明确,提升了移植性和可扩展性。
- 设备树驱动模型进一步增强了解耦,硬件信息存储在设备树中,驱动可以根据硬件的描述自动配置,具有更高的灵活性,适合现代的嵌入式平台。
10.2 设备树
设备树(Device Tree,DT) 是一种用于描述硬件的结构化数据,它独立于驱动程序,提供了一种可移植的方式来描述硬件资源。Linux内核使用设备树来解析硬件信息,使得驱动代码可以更好地解耦,并支持不同的硬件平台。
10.2.1 设备树基本结构
设备树文件以 .dts
(设备树源码)和 .dtsi
(设备树头文件)形式存在,编译后会生成 .dtb
(设备树二进制文件),在系统启动时由内核解析并加载。
(1)节点类型
设备树的结构是 层级化的树形结构,由多个节点组成:
- 根节点
/
:设备树的起点,代表整个系统。 - 普通节点:用于描述具体的设备,每个节点的名称通常遵循格式:
label:node-name@address
node-name
:设备名称@address
:设备的物理基地址(可选)label:
:给该节点赋予一个标签(可选),其他地方使用&label
引用。
例如:
/ {
model = "My Embedded Board";
compatible = "myvendor,myboard";
memory1:memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; // 起始地址 0x80000000,大小 256MB
};
};
(2)常用属性
-
compatible
:设备与驱动的匹配标识,驱动会根据该字段匹配相应的设备。compatible = "myvendor,my-device";
-
reg
:定义设备寄存器的地址和大小。reg = <0x1000 0x100>; // 基地址 0x1000,大小 0x100
-
interrupts
:定义设备的中断号。interrupts = <5>; // 中断号 5
-
status
:设备状态,okay
代表启用,disabled
代表禁用。status = "okay";
-
phandle
:节点的唯一标识符,可以在设备树内部引用。
10.2.2 子系统
(1)pinctrl子系统(引脚复用)
作用:
- 现代嵌入式系统中的引脚(GPIO、UART、I2C、SPI等)通常是复用的,需要配置成正确的模式才能正常工作。
pinctrl
子系统用于管理引脚的复用功能,设备树可通过pinctrl-0
等属性指定使用哪些引脚配置。
设备树配置示例:
&iomuxc {
pinctrl_uart1: uart1grp {
fsl,pins = <
MX6QDL_PAD_CSI0_DAT10__UART1_TX_DATA 0x1b0b1
MX6QDL_PAD_CSI0_DAT11__UART1_RX_DATA 0x1b0b1
>;
};
};
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
status = "okay";
};
解析:
iomuxc
:代表 I/O 复用控制模块(SoC 中的 IOMUX Controller)。pinctrl_uart1
:定义了一组 UART1 的引脚配置。fsl,pins
:用于指定具体的引脚和模式。pinctrl-names
和pinctrl-0
:在&uart1
设备节点中引用pinctrl_uart1
,确保 UART1 正确使用该引脚配置。
(2)GPIO子系统
作用:
GPIO(通用输入输出)用于控制外部设备,例如 LED、按键、继电器等。Linux 提供了 GPIO 子系统,允许用户空间和驱动程序操作 GPIO 设备。
设备树配置:
leds {
compatible = "gpio-leds";
led1 {
gpios = <&gpio1 17 GPIO_ACTIVE_LOW>;
label = "status-led";
};
};
解析:
compatible = "gpio-leds"
:该设备是一个 GPIO LED 设备,使用 Linux 的gpio-leds
驱动。gpios = <&gpio1 17 GPIO_ACTIVE_LOW>
:&gpio1
:表示 GPIO 控制器。17
:GPIO 端口编号。GPIO_ACTIVE_LOW
:表示低电平点亮。
GPIO 关键函数:
在驱动程序中,可以使用以下 API 操作 GPIO:
int gpio_request(unsigned int gpio, const char *label); // 请求 GPIO 资源
int gpio_direction_input(unsigned int gpio); // 配置 GPIO 为输入模式
int gpio_direction_output(unsigned int gpio, int value); // 配置 GPIO 为输出模式
int gpio_get_value(unsigned int gpio); // 读取 GPIO 输入值
void gpio_set_value(unsigned int gpio, int value); // 设置 GPIO 输出值
void gpio_free(unsigned int gpio); // 释放 GPIO 资源
驱动示例:
static int led_probe(struct platform_device *pdev) {
int gpio = of_get_named_gpio(pdev->dev.of_node, "gpios", 0);
gpio_request(gpio, "status-led");
gpio_direction_output(gpio, 1); // 默认熄灭
return 0;
}
10.2.3 设备树的匹配机制
Linux 设备树驱动使用 compatible
进行匹配,驱动代码中通常包含 设备树匹配表:
设备树部分
&spi1 {
compatible = "myvendor,my-spi";
reg = <0x40000000 0x1000>;
status = "okay";
};
驱动部分
static const struct of_device_id my_spi_of_match[] = {
{ .compatible = "myvendor,my-spi" },
{},
};
static struct platform_driver my_spi_driver = {
.driver = {
.name = "my_spi",
.of_match_table = my_spi_of_match,
},
.probe = my_spi_probe,
.remove = my_spi_remove,
};
module_platform_driver(my_spi_driver);
解析:
- 设备树
compatible = "myvendor,my-spi"
。 - 驱动程序的
of_device_id my_spi_of_match[]
进行匹配。 platform_driver
通过of_match_table
指定匹配表,使得设备树节点能正确关联到驱动。
10.3 常用机制
在 Linux 设备驱动开发中,中断处理、I/O 机制和内存映射(mmap)是至关重要的部分。不同的硬件设备可能采用不同的方式进行数据交互和事件处理。
10.3.1 中断处理机制
中断是一种异步事件处理方式,使 CPU 能够在设备请求时立即响应,而不是等待设备的完成状态。Linux 内核的中断处理分为 硬件中断和软件中断,并采用 顶半部(Top Half)和底半部(Bottom Half) 机制来优化处理流程。
中断分类
(1)硬件中断(IRQ, Interrupt Request)
- 由外设(键盘、网卡、定时器等)触发。
- 需要 CPU 响应,执行中断处理函数。
- 适用于所有需要 CPU 及时响应的设备,如 GPIO、串口、网络设备。
(2)软件中断
- 由 软件指令 触发,主要用于 定时任务 或 内核异步任务 处理,例如:
- SoftIRQ(软中断):用于网络包处理、定时器、SCSI 任务等。
- Tasklet(任务快):用于轻量级任务,不能在多 CPU 上并行执行。
- Workqueue(工作队列):用于较复杂的任务,支持 睡眠操作。
中断处理分层
Linux 的中断处理采用 顶半部和底半部 机制,使 CPU 能够快速响应外部设备。
(1)顶半部(Top Half)
- 执行关键性任务(如清除中断标志位、读取数据)。
- 不能执行 耗时操作,不能调用可能 睡眠 的函数。
注册中断处理函数(request_irq)
static irqreturn_t my_irq_handler(int irq, void *dev_id) {
printk(KERN_INFO "Interrupt occurred!\n");
return IRQ_HANDLED;
}
static int __init my_driver_init(void) {
int irq = 5; // 设备中断号
return request_irq(irq, my_irq_handler, IRQF_SHARED, "my_device", NULL);
}
(2)底半部(Bottom Half)
底半部用于处理 耗时任务,避免阻塞 CPU。Linux 提供了 Tasklet、工作队列(Workqueue)和中断线程化 机制。
Tasklet(轻量级任务)
- 特点:不可睡眠,适用于简单操作。
-
使用方式:
void my_tasklet_func(unsigned long data) { printk(KERN_INFO "Tasklet executed\n"); } DECLARE_TASKLET(my_tasklet, my_tasklet_func, 0); static irqreturn_t my_irq_handler(int irq, void *dev_id) { tasklet_schedule(&my_tasklet); return IRQ_HANDLED; }
工作队列(Workqueue,支持睡眠)
- 适用于 I/O 操作或复杂任务,可以调度到内核线程中运行。
-
示例:
void my_workqueue_func(struct work_struct *work) { printk(KERN_INFO "Workqueue executed\n"); } DECLARE_WORK(my_work, my_workqueue_func); static irqreturn_t my_irq_handler(int irq, void *dev_id) { schedule_work(&my_work); return IRQ_HANDLED; }
中断线程化
- 适用于 实时系统,将中断处理放入内核线程中。
-
示例:
static irqreturn_t my_threaded_irq_handler(int irq, void *dev_id) { printk(KERN_INFO "Threaded IRQ Handler\n"); return IRQ_HANDLED; } request_irq(irq, NULL, IRQF_ONESHOT | IRQF_SHARED, "my_device", NULL);
10.3.2 I/O 处理机制
I/O 机制决定了 用户进程 如何与设备进行交互。常见的 I/O 处理方式包括:
- 轮询(Polling)
- 休眠/唤醒(Wait & Wake)
- Poll 机制
- 异步通信(Async I/O)
轮询(Polling)
- CPU 持续查询设备状态,直到设备准备好。
- 缺点:浪费 CPU 资源,效率低。
- 适用于:简单 I/O 设备,如 GPIO 输入检测。
while (!device_ready()) {
cpu_relax(); // 释放 CPU,降低功耗
}
休眠/唤醒机制
进程在设备未准备好时进入睡眠,等待事件发生后被唤醒。
关键 API
wait_event_interruptible()
进入休眠。-
wake_up_interruptible()
唤醒进程。示例
DECLARE_WAIT_QUEUE_HEAD(my_queue);
int flag = 0;
static int my_read_function(void) {
wait_event_interruptible(my_queue, flag != 0);
return 0;
}
static void my_irq_handler(int irq, void *dev_id) {
flag = 1;
wake_up_interruptible(&my_queue);
}
Poll 机制
Poll 结合 轮询和中断,避免 CPU 资源浪费。
关键 API
-
poll_wait()
注册等待队列。实现
file_operations.poll
static unsigned int my_poll(struct file *filp, poll_table *wait) { poll_wait(filp, &my_queue, wait); return (flag) ? POLLIN | POLLRDNORM : 0; }
异步通信
-
采用信号(Signal)或
io_uring
实现非阻塞 I/O 处理。使用
struct fasync_struct
进行信号通知
static struct fasync_struct *async_queue;
static int my_fasync(int fd, struct file *filp, int mode) {
return fasync_helper(fd, filp, mode, &async_queue);
}
static void my_irq_handler(int irq, void *dev_id) {
kill_fasync(&async_queue, SIGIO, POLL_IN);
}
10.3.3 内存映射(mmap)机制
mmap 允许用户进程直接访问设备内存,提高数据传输效率,避免拷贝开销。
- 映射设备内存到用户空间,用户进程可以直接访问硬件地址。
实现 file_operations.mmap
static int my_mmap(struct file *filp, struct vm_area_struct *vma) {
return remap_pfn_range(vma, vma->vm_start,
virt_to_phys(device_memory) >> PAGE_SHIFT,
vma->vm_end - vma->vm_start,
vma->vm_page_prot);
}
关键结构体
struct vm_area_struct
:描述进程的虚拟内存区域。
资源
版本管理
版本 | 时间 | 描述 |
---|---|---|
v1.0.0 | 2025.1.28 | 初始版本 |
v1.1.0 | 2025.1.30 | 更新5 |
v1.2.0 | 2025.1.31 | 更新6 |
v1.3.0 | 2025.2.01 | 更新7 |
v1.4.0 | 2025.2.03 | 更新8 |
v1.5.0 | 2025.2.04 | 更新9 |
v1.6.0 | 2025.2.07 | 更新10 |