Linux入门笔记




1. 系统启动

Linux 系统启动分为三个主要阶段:Bootloader内核用户空间初始化


1.1 Bootloader 阶段

  • 作用:加载操作系统内核并将控制权移交给内核。
  • 过程
    1. BIOS/UEFI 初始化硬件并找到启动设备。
    2. Bootloader(如 GRUB)加载 Linux 内核和初始文件系统(initramfs)。
    3. 向内核传递启动参数。

1.2 内核阶段

  • 作用:初始化硬件并启动系统的核心功能。
  • 过程
    1. 解压内核并启动硬件检测。
    2. 加载设备驱动并挂载临时根文件系统(initramfs)。
    3. 启动第一个用户空间进程(/sbin/init 或 Systemd)。

1.3 用户空间初始化

  • 作用:加载服务并提供用户交互环境。
  • 过程
    1. 启动初始化系统(Systemd 或 SysVinit)。
    2. 加载系统服务(如网络、日志、图形界面)。
    3. 提供用户登录界面,进入工作环境。

1.4 程序运行

  • 作用:用户可以运行自己的程序或使用系统应用。
  • 过程
    1. 用户登录后,Shell(如 Bash)作为命令解释器加载。
    2. 用户可以运行程序(如 vim、python 或 GUI 应用)。
    3. 程序通过系统调用与内核交互完成任务。

2. 万物皆文件的概念

“万物皆文件” 是 Linux 系统的核心思想之一。

它的含义是:在 Linux 中,几乎所有东西都可以抽象为文件,这包括硬件设备、进程、网络接口等。通过统一的文件接口,Linux 提供了一种简单而高效的方式与系统交互。


2.1 什么是“万物皆文件”?

在 Linux 系统中,以下几类实体都被当作文件对待:

  1. 普通文件:常见的文本文件或二进制文件。
  2. 目录:目录本质上是特殊的文件,用于保存文件的名称和位置。
  3. 设备文件:硬件设备(如硬盘、显示器、键盘等)在 /dev 中表示为文件。
  4. 管道和套接字:用于进程间通信的特殊文件。
  5. 内核和进程信息:通过虚拟文件系统 /proc/sys,可以以文件的形式访问内核和进程的状态。

2.2 文件的种类

在 Linux 中,不同类型的“文件”代表不同的资源:

  1. 普通文件
    • /etc/passwd/var/log/syslog
    • 存储数据的普通文件,可以直接读取或写入。
  2. 目录
    • /home/username/var/log
    • 存储其他文件的列表。
  3. 设备文件
    • 位于 /dev 目录下,表示硬件设备。
    • /dev/sda(磁盘设备)、/dev/tty(终端)。
    • 分为两类:
      • 块设备(如硬盘)。
      • 字符设备(如键盘、鼠标)。
  4. 虚拟文件
    • 位于 /proc/sys,由内核动态生成。
    • /proc/cpuinfo(CPU 信息)、/sys/class/net/(网络接口)。
  5. 管道和套接字
    • 进程间通信的文件接口。
    • 如:命令之间的管道 |

2.3 统一文件接口的优势

Linux 系统提供了一种统一的方式对待所有资源:

  1. 简单性
    • 不需要为每种设备或资源设计单独的接口,所有资源都通过文件的读写操作进行访问。
    • 无论是读取文本文件,还是与硬件交互,使用的都是相同的系统调用(如 openreadwrite)。
  2. 扩展性
    • 新增设备或资源时,只需实现与文件相关的接口即可,方便扩展和维护。
  3. 灵活性
    • 管道和重定向等机制基于文件的抽象提供强大的组合能力。

3. 挂载的概念

挂载(Mount) 是 Linux 文件系统中非常重要的概念。它的作用是将一个设备(如硬盘分区、U 盘、光盘等)的文件系统连接到当前的目录树上,使用户可以通过挂载点访问设备上的内容。


3.1 什么是挂载?

在 Linux 中,硬件设备(如磁盘分区、网络存储)被抽象为文件,而挂载是将设备中的文件系统挂载到某个目录(挂载点)上,从而通过该目录访问设备内容。

  • 硬盘分区本身(如 /dev/sda1)只是一个块设备,无法直接读取文件。
  • 挂载会将设备的文件系统映射到一个目录(如 /mnt/data)。
  • 挂载后,用户通过目录树(挂载点)访问该设备的内容。

3.2 挂载的过程

  1. 找到设备文件

    • 在 Linux 中,每个硬件设备都会表示为一个设备文件,通常位于 /dev 目录下。
    • 例如:
      • /dev/sda1:磁盘的第一个分区。
      • /dev/sr0:光驱设备。
  2. 挂载设备到挂载点

    • 将设备挂载到某个目录(挂载点)上,例如 /mnt/data
    • 挂载后,访问 /mnt/data 就相当于访问设备上的文件系统。
  3. 使用挂载命令

    • 使用 mount 命令进行挂载:
      sudo mount /dev/sda1 /mnt/data
    • 挂载后,/mnt/data 目录的内容就会显示为 /dev/sda1 分区上的文件。
  4. 卸载设备

    • 卸载时,使用 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 挂载点的特点

  1. 挂载点是一个目录,可以是空目录,也可以是已有内容的目录。
  2. 挂载后,挂载点原有内容会被隐藏,直到设备被卸载。
    • 示例:
      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 挂载的用途

  1. 访问存储设备
    • 将硬盘分区、U 盘等挂载到文件系统中,方便访问。
  2. 网络存储挂载
    • 通过挂载 NFS、CIFS 等协议访问网络文件系统。
  3. 设备挂载
    • 挂载光盘、ISO 镜像等设备。
  4. 特殊文件系统
    • 挂载虚拟文件系统(如 /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

/sbin(系统管理命令)

  • 作用

    • 存放系统管理工具的二进制文件(仅超级用户可用)。
    • 包含用于系统启动、修复和恢复的命令。
  • 示例命令

    • fsck:文件系统检查。
    • reboot:重启系统。
  • 特点

    • 在 Ubuntu 22 中,/sbin/usr/sbin 的符号链接:
      /sbin -> /usr/sbin

/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 文件)。
  • 子目录
    • /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

永久修改(影响所有终端)

  1. 用户级(仅影响当前用户)
配置文件 适用 Shell 作用
~/.bashrc Bash 交互式终端 每次打开终端都会加载
~/.profile 登录 Shell(GUI & SSH) 每次用户登录都会加载

示例(添加环境变量):

echo 'export PATH=$PATH:/home/user/bin' >> ~/.bashrc
source ~/.bashrc
  1. 系统级(影响所有用户)
配置文件 作用
/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.cb.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 变量和条件判断

  1. 简单变量定义:

    A = xxx  # A 的值在定义时确定
    B := yyy  # B 的值在使用时确定
    C ?= zzz  # C 的值在定义时确定,如果未定义则使用 zzz
    D += uuu  # D 的值在定义时确定,如果已定义则追加 uuu
  2. 条件判断:
    这段 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
  1. 条件判断

    • ifeq ($(CC), gcc):这行代码的意思是检查 $(CC) 变量的值是否等于 gcc$(CC) 变量通常表示编译器的名称,如果使用的是 gcc 编译器,那么这个条件成立。
  2. 执行的动作

    • 如果 $(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:匹配的模式,可以是通配符(例如 *.csrc/*.c 等)。

    示例

    SRC_FILES = $(wildcard src/*.c)
    $(info SRC_FILES = $(SRC_FILES))  # 输出匹配到的文件列表
  • 假设 src 目录下有 a.cb.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 文件。
  • 用于格式化路径或字符串。

realpathabspath 函数

功能

  • 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 系统调用流程

在操作系统的设计中,系统调用是用户程序和操作系统内核之间的接口,允许用户程序请求内核执行某些特权操作,如文件管理、内存分配、进程管理等。

  1. 用户态到内核态的切换

    1. 用户程序将参数存入寄存器

      • 用户程序通过调用系统调用接口(如 openreadwrite)来请求操作系统执行某些任务。系统调用需要参数,这些参数通常包括文件路径、读写缓冲区、文件描述符等。
      • 这些参数会存入 CPU 寄存器 中。用户程序将其需要传递给内核的参数放在特定的寄存器中,以便在执行系统调用时内核能够读取。
    2. 触发软中断(软中断指令)

      • 执行 syscall(或在某些系统中为 int 0x80)指令时,程序会触发一个 软中断,这是一个软中断(与硬件中断不同)信号,它通知 CPU 需要切换到内核态。
      • 软中断通过执行特殊的指令(如 syscall 指令)让 CPU 跳转到内核空间,调用操作系统内核的代码。这一过程是从用户空间到内核空间的切换,进入到特权模式。
    3. CPU 切换到内核态

      • 在触发软中断之后,CPU 会从 用户态 切换到 内核态。这时,程序的控制权不再属于用户空间,而是进入内核空间。
      • 内核空间是操作系统的运行环境,它具有访问硬件、管理内存和控制进程的权限。用户程序通过系统调用进入内核空间,操作系统通过这些接口来保证系统的安全性和稳定性。
    4. 跳转到系统调用处理程序

      • 当 CPU 进入内核态时,它会跳转到相应的 系统调用处理程序。不同的系统调用会触发不同的处理程序。
      • 这些处理程序会根据传入的参数执行实际的操作。例如,open 系统调用的处理程序会打开一个文件,read 系统调用会从文件中读取数据,write 系统调用会向文件写入数据。
  2. 内核态到用户态的切换

    1. 内核验证参数并执行操作

      • 一旦 CPU 进入内核空间,内核会对传入的参数进行验证(例如,检查文件路径的有效性、验证文件的访问权限等)。
      • 内核执行完相关的操作后,它将结果存储在适当的位置(例如返回值、错误码等)。在文件 I/O 系统调用中,操作系统会操作文件系统、读写磁盘,并更新文件描述符表。
    2. 将结果返回给用户程序

      • 内核完成操作后,它会将结果返回给用户程序。这些结果通常存储在寄存器中,并通过系统调用的返回值传递给用户空间。
      • 例如,在 read 系统调用中,返回值通常是成功读取的字节数,或者在发生错误时返回 -1
    3. 切换回用户态

      • 在操作完成后,操作系统会通过 返回指令(例如 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() 控制同步信号量。

  • Socketsocket()/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 系统中,从用户层到内核层的文件读取操作流程如下:

  1. 用户层程序调用 fopen("file.txt", "r")(系统函数)

  2. glibc 将其转换为系统调用 open(),并传递给内核。

  3. 内核通过 虚拟文件系统(VFS) 查找文件系统类型(如 ext4)。

  4. 文件系统驱动访问对应的存储设备(如硬盘)。

  5. 硬盘的控制器通过 DMA(直接内存访问) 将数据传输到内存。

  6. 数据返回给用户层,程序通过 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-namespinctrl-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 处理方式包括:

  1. 轮询(Polling)
  2. 休眠/唤醒(Wait & Wake)
  3. Poll 机制
  4. 异步通信(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:描述进程的虚拟内存区域。

资源

【韦东山】韦东山手把手教你嵌入式Linux快速入门到精通

版本管理

版本 时间 描述
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
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇