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各自怎么玩,以及怎么让它们高效对话。