结构体与联合体在操作系统中的应用
说实话,很多学C语言的朋友都觉得结构体和联合体就是个“高级点的数组”。但等你真正去啃操作系统源码时,你会发现——整个操作系统的核心数据结构,几乎都是用结构体和联合体搭起来的。
今天我就带你看看,这三个经典场景:进程控制块(PCB)、文件描述符表、系统调用参数。它们是怎么用结构体和联合体实现的,以及我踩过的那些坑。
一、进程控制块(PCB)——操作系统的“身份证”
每个进程在操作系统里都有一个“身份证”,这就是PCB。我当年第一次看Linux源码时,差点被task_struct吓到——好家伙,几百行!但说白了,它就是个巨大的结构体。
核心要点:PCB是结构体的典型应用,它把进程的所有属性打包在一起。
// 简化的进程控制块
struct task_struct {
// 进程状态
volatile long state; // -1 不可运行, 0 可运行, >0 已停止
// 进程ID
pid_t pid;
// 进程调度信息
unsigned int policy; // 调度策略
int prio; // 动态优先级
int static_prio; // 静态优先级
// 内存管理
struct mm_struct *mm; // 进程地址空间
// 文件系统信息
struct fs_struct *fs; // 文件系统信息
// 打开的文件
struct files_struct *files; // 文件描述符表
// 信号处理
struct signal_struct *signal;
// 时间统计
cputime_t utime, stime; // 用户态、内核态时间
};
你看,这个结构体里什么都有:状态、ID、调度信息、内存、文件、信号……一个进程需要的所有东西,全在这里了。
我个人习惯在写嵌入式RTOS时,把PCB设计得更精简一些。比如这样:
// 嵌入式RTOS中的精简PCB
typedef struct {
uint32_t *sp; // 栈指针
uint32_t stack_size; // 栈大小
uint8_t priority; // 优先级
uint8_t state; // 状态:就绪、运行、阻塞
char name[16]; // 进程名
void (*entry)(void*); // 入口函数
void *args; // 参数
} os_pcb_t;
我的经验:在资源受限的MCU上,PCB别搞太复杂。我曾经在一个项目里把PCB设计成Linux那样,结果RAM直接爆了。后来精简到10个字段,跑得稳稳的。
二、文件描述符表——结构体数组的妙用
文件描述符表,说白了就是一个结构体数组。每个进程打开一个文件,就在这个数组里占一个位置。
为什么会用结构体数组?因为每个文件描述符需要记录的信息不止一个:文件指针、访问模式、状态标志……这些用结构体来组织再合适不过了。
// 文件描述符表的核心结构
struct file {
mode_t f_mode; // 文件访问模式
loff_t f_pos; // 文件读写位置
unsigned int f_flags; // 文件状态标志
struct file_operations *f_op; // 文件操作函数表
void *private_data; // 私有数据
atomic_t f_count; // 引用计数
};
// 每个进程的文件描述符表
struct files_struct {
atomic_t count; // 引用计数
struct file **fd_array; // 文件描述符数组
int max_fds; // 最大文件描述符数
};
嗯,这里要注意一个细节:fd_array是个二级指针。为什么?因为文件描述符表是动态增长的。一开始只分配几个,不够了再扩展。我见过不少新手直接写死数组大小,结果文件一多就崩了。
避坑指南:我曾经在一个网络服务器项目里,把fd_array写成固定大小256。结果客户一上线就开了300个连接,程序直接段错误。后来改成动态分配,问题解决。
三、系统调用参数——联合体的经典用法
系统调用是用户程序进入内核的“大门”。但问题来了:不同的系统调用,需要的参数类型和数量都不一样。怎么办?
答案就是:联合体。
// 系统调用参数联合体
union syscall_args {
// 文件操作
struct {
const char *pathname;
int flags;
mode_t mode;
} open;
// 内存操作
struct {
void *addr;
size_t length;
int prot;
int flags;
} mmap;
// 进程控制
struct {
pid_t pid;
int sig;
} kill;
// 网络操作
struct {
int sockfd;
struct sockaddr *addr;
socklen_t addrlen;
} connect;
};
// 系统调用入口
long syscall_handler(int nr, union syscall_args *args) {
switch (nr) {
case SYS_OPEN:
return sys_open(args->open.pathname,
args->open.flags,
args->open.mode);
case SYS_MMAP:
return sys_mmap(args->mmap.addr,
args->mmap.length,
args->mmap.prot,
args->mmap.flags);
// ...
}
}
你看,联合体在这里的作用就是节省空间。所有系统调用的参数共用一块内存,一次只用一个。这比用结构体省多了——如果用结构体,你得把每种系统调用的参数都列出来,那得多大?
核心思想:联合体适合“多选一”的场景。系统调用参数、协议解析、寄存器模拟……这些地方用联合体,既清晰又高效。
四、知识体系总览
下面这张图,帮你理清结构体和联合体在操作系统中的角色:
五、实际项目中的组合用法
在实际的操作系统代码里,结构体和联合体经常嵌套使用。比如,PCB里可能包含一个联合体,用来表示进程的不同状态:
// 进程状态联合体
union process_state {
struct {
uint8_t running : 1;
uint8_t ready : 1;
uint8_t blocked : 1;
uint8_t zombie : 1;
} bits;
uint8_t value;
};
// PCB中使用联合体
typedef struct {
char name[32];
pid_t pid;
union process_state state; // 状态用联合体表示
uint32_t *stack_ptr;
// ...
} pcb_t;
这样设计的好处是:既可以用位域单独操作每个状态位,也可以整体读写状态值。我在调试时经常直接打印state.value,一眼就能看出进程处于什么状态。
小技巧:用联合体+位域的组合,可以做到“既能原子操作,又能位操作”。这在操作系统的同步机制里特别有用。
六、总结
好了,今天的内容就到这里。结构体和联合体在操作系统里的应用,说白了就是:
- PCB:用结构体把进程的所有属性打包在一起,方便管理
- 文件描述符表:用结构体数组管理多个打开的文件
- 系统调用参数:用联合体让不同系统调用共用参数空间
这些设计思想,你在写嵌入式系统、驱动开发、甚至大型应用时都能用上。下次写代码时,不妨想想:这里用结构体还是联合体?或者两者结合?