3、Linux系统基础:Linux内核架构、进程管理、内存管理、文件系统

好,咱们进入Linux系统基础这一章。说实话,很多做嵌入式尤其是QNX出身的朋友,刚接触Linux时都会觉得有点别扭——怎么进程管理这么重?内存管理还搞虚拟地址?文件系统怎么啥都是文件?

我个人习惯是,先别急着写代码,把内核的骨架摸清楚。你想想看,双系统协同开发,你至少得知道Linux这边是怎么调度任务的、怎么分配内存的,不然两边数据交互时出了性能问题,你都不知道该查哪一头。

3.1 Linux内核架构概览

Linux内核,说白了就是一个宏内核(Monolithic Kernel)。什么意思呢?就是所有核心功能——进程调度、内存管理、文件系统、网络协议栈——全都在内核空间里跑。这和QNX的微内核思路完全不同。

核心区别:Linux宏内核 = 大而全,性能高但模块耦合紧;QNX微内核 = 小而精,模块独立但IPC开销大。

我在项目中遇到过这样一个场景:一个数据采集系统,QNX负责实时控制,Linux负责网络通信和日志存储。刚开始我们没注意内核架构差异,结果Linux这边一个文件系统IO就把整个系统卡住了。后来才意识到,Linux的宏内核里,文件系统和调度器是共享地址空间的,一个慢操作会影响全局。

Linux内核的主要子系统包括:

  • 进程调度器(Scheduler):决定哪个进程获得CPU
  • 内存管理器(MM):虚拟内存、页表、缺页中断
  • 虚拟文件系统(VFS):统一文件操作接口
  • 网络协议栈:TCP/IP、套接字
  • 设备驱动框架:字符设备、块设备、网络设备

嗯,这里要注意:Linux内核虽然是宏内核,但支持模块化加载。你可以把驱动编译成.ko文件,运行时动态插入。这一点在双系统开发中特别有用——比如你只需要在Linux侧挂载一个特定的文件系统,完全没必要重新编译整个内核。

3.2 进程管理:调度与上下文切换

Linux的进程管理,核心是CFS(完全公平调度器)。这个名字起得好,但实际用起来你得知道它的脾气。

CFS的思路很简单:每个进程分配一个虚拟运行时间(vruntime),调度器总是选vruntime最小的进程来跑。说白了,就是尽量让每个进程都得到公平的CPU时间。

个人经验:我在调试一个多线程数据转发模块时,发现某个线程总是抢不到CPU。查了半天,原来是它的nice值设得太高了(优先级低),CFS自然就不怎么给它时间片。调低nice值后,问题解决。记住,CFS是公平,不是平均——优先级高的进程还是能多拿时间。

进程状态转换图,大家应该都见过:

运行态(TASK_RUNNING)
   ↓  ↑
就绪态 ←→ 睡眠态(TASK_INTERRUPTIBLE)
   ↓
停止态(TASK_STOPPED)
   ↓
僵尸态(TASK_ZOMBIE)

这里有个坑,我曾经踩过:僵尸进程。子进程退出了,但父进程没调用wait()回收资源,子进程的进程描述符就留在内核里。在双系统场景下,如果Linux侧频繁创建销毁子进程,又不及时回收,内核内存会慢慢泄漏。我建议在代码里加一个SIGCHLD信号处理,或者直接用waitpid()非阻塞轮询。

进程管理相关的关键系统调用:

系统调用 功能 双系统开发中的用途
fork() 创建子进程 启动数据处理子任务
exec() 加载新程序 切换执行不同模块
wait() 等待子进程结束 同步子任务完成
sched_yield() 主动让出CPU 避免忙等,配合QNX实时任务

3.3 内存管理:虚拟地址与页表

Linux的内存管理,核心是虚拟内存。每个进程都以为自己独占4GB(32位)或超大虚拟空间(64位),实际上物理内存是共享的。这个机制靠页表(Page Table)和MMU(内存管理单元)实现。

你想想看,为什么要有虚拟内存?

  • 隔离性:进程A不能随便访问进程B的内存
  • 简化编程:程序员不用管物理内存碎片
  • 按需加载:只有用到的页面才映射到物理内存

避坑指南:我曾经在双系统共享内存时犯过一个低级错误。QNX侧分配了一段物理连续内存,然后通过PCIe BAR映射给Linux。结果Linux侧用mmap()映射后,访问时触发缺页中断,性能直接崩了。原因?Linux的mmap默认是惰性映射,不会立即建立页表。解决方案是加MAP_POPULATE标志,强制预分配物理页面。

内存分配的两条路径:

  • malloc() / free():用户态,通过brk()或mmap()实现
  • kmalloc() / vmalloc():内核态,kmalloc分配物理连续内存,vmalloc分配虚拟连续但物理可能不连续

在双系统开发中,我建议你重点关注大页(Huge Pages)。Linux默认页面大小是4KB,但如果你要共享大块数据(比如视频帧、雷达点云),4KB页面会导致TLB(页表缓存)频繁失效。启用2MB甚至1GB的大页,能显著提升性能。

# 查看当前大页配置
cat /proc/meminfo | grep Huge

# 临时启用大页(2MB)
echo 1024 > /proc/sys/vm/nr_hugepages

# 在代码中使用大页
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                 MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);

3.4 文件系统:VFS与具体实现

Linux的文件系统,最牛的地方是VFS(虚拟文件系统)层。它定义了一套统一的接口——open、read、write、close——不管底层是ext4、XFS、还是NFS,上层应用都一视同仁。

嗯,这里有个关键点:在Linux里,一切皆文件。设备是文件(/dev/xxx)、进程信息是文件(/proc/xxx)、甚至内核参数也是文件(/sys/xxx)。这个设计哲学,和QNX的资源管理器(Resource Manager)有异曲同工之妙。

常见的文件系统类型:

文件系统 特点 适用场景
ext4 成熟稳定,支持日志 通用嵌入式存储
XFS 高并发,大文件性能好 数据采集、日志存储
tmpfs 基于内存,掉电丢失 临时数据、共享缓存
NFS 网络文件系统 双系统间文件共享

我的建议:在双系统协同开发中,如果QNX和Linux需要共享文件,别用NFS——延迟太大。我一般用tmpfs或者直接共享物理内存区域,然后在Linux侧用/dev/mem或uio驱动来访问。速度快得多。

文件系统操作的核心系统调用:

// 打开文件
int fd = open("/path/to/file", O_RDWR | O_CREAT, 0644);

// 读写文件
ssize_t n = read(fd, buf, sizeof(buf));
ssize_t m = write(fd, buf, sizeof(buf));

// 定位文件
off_t pos = lseek(fd, 0, SEEK_SET);

// 同步到磁盘
fsync(fd);

// 关闭文件
close(fd);

最后说一个我踩过的坑:文件系统缓存。Linux默认会把文件数据缓存在内存中(Page Cache),写操作先写缓存,再异步刷到磁盘。这在普通场景下没问题,但如果你在双系统间共享存储设备(比如eMMC),QXN侧写完了数据,Linux侧读到的可能是旧缓存。解决办法是:在Linux侧打开文件时用O_DIRECT标志,绕过Page Cache,或者写完后调用fsync()强制刷盘。

好了,Linux系统基础就讲到这里。下一章我们会深入进程间通信(IPC),看看QNX和Linux各自怎么玩,以及怎么让它们高效对话。