Linux 0.11进程状态追踪实战:手把手教你用fprintk给内核打日志(附Python分析脚本)
在操作系统内核开发中,理解进程的生命周期和状态转换是至关重要的。Linux 0.11作为早期Linux内核版本,其简洁的设计和清晰的代码结构使其成为学习操作系统原理的理想选择。本文将带你深入探索如何在Linux 0.11内核中插入日志打印函数,实时追踪进程从创建到消亡的完整生命周期。
1. 环境准备与基础概念
在开始修改内核代码之前,我们需要先了解一些基本概念和准备工作。Linux 0.11中的进程状态主要包括以下几种:
- N (新建):进程刚被创建时的状态
- J (就绪):进程准备好运行,等待CPU调度
- R (运行):进程正在CPU上执行
- W (等待):进程因等待资源而进入睡眠状态
- E (退出):进程结束运行
为了追踪这些状态变化,我们需要在内核的关键调度函数中插入日志打印语句。这里我们使用fprintk函数,它是内核中专门用于打印日志的函数。
注意:在修改内核代码前,建议先备份原始文件,以便出现问题时可以快速恢复。
2. 内核代码修改实战
2.1 初始化日志文件
首先,我们需要在内核初始化时创建日志文件。修改main.c文件(位于linux-0.11/init/目录):
// 在init函数中增加以下语句 (void) open("/var/process.log", O_CREAT|O_TRUNC|O_WRONLY, 0666);这里有几个关键点需要注意:
- 文件打开操作必须放在
init函数内部,不能放在函数之前 - 文件路径设置为
/var/process.log - 使用
O_CREAT|O_TRUNC标志确保每次启动都会创建新文件
重要提示:不要在
task[0]中执行文件操作,这可能导致系统崩溃。task[0]是系统的第一个进程,只负责创建其他进程。
2.2 修改fork.c追踪进程创建
在fork.c文件中,我们需要在进程创建的关键点插入日志打印:
int copy_process(int nr, long ebp, long edi, long esi, long gs, long none, long ebx, long ecx, long edx, long fs, long es, long ds, long eip, long cs, long eflags, long esp, long ss) { struct task_struct *p; // 分配内存给新进程 p = (struct task_struct *) get_free_page(); // 设置进程状态为不可中断 p->state = TASK_UNINTERRUPTIBLE; fprintk(3, "%ld\tN\t%ld\n", p->pid, jiffies); // ... 其他初始化代码 ... // 最后将进程状态设为就绪 p->state = TASK_RUNNING; fprintk(3, "%ld\tJ\t%ld\n", p->pid, jiffies); return last_pid; }2.3 修改sched.c追踪进程调度
schedule()函数是内核调度的核心,我们需要在这里添加多个日志点:
void schedule(void) { int i, next, c; struct task_struct **p; // 检查信号和闹钟 for(p = &LAST_TASK; p > &FIRST_TASK; --p) { if (*p) { if ((*p)->alarm && (*p)->alarm < jiffies) { (*p)->signal |= (1<<(SIGALRM-1)); (*p)->alarm = 0; } if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && (*p)->state == TASK_INTERRUPTIBLE) { (*p)->state = TASK_RUNNING; fprintk(3, "%d\tJ\t%d\n", (*p)->pid, jiffies); } } } // 调度器主循环 while (1) { c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS]; while (--i) { if (!*--p) continue; if ((*p)->state == TASK_RUNNING && (*p)->counter > c) { c = (*p)->counter; next = i; } } if (c) break; for(p = &LAST_TASK; p > &FIRST_TASK; --p) { if (*p) { (*p)->counter = ((*p)->counter >> 1) + (*p)->priority; } } } // 进程切换 if (current->pid != task[next]->pid) { if (current->state == TASK_RUNNING) { fprintk(3, "%d\tJ\t%d\n", current->pid, jiffies); } fprintk(3, "%d\tR\t%d\n", task[next]->pid, jiffies); } switch_to(next); }3. 进程状态转换的完整追踪
3.1 睡眠和唤醒操作
除了基本的创建和调度,我们还需要追踪进程的睡眠和唤醒操作:
// 在sys_pause()函数中 int sys_pause(void) { if (current->state != TASK_INTERRUPTIBLE) { fprintk(3, "%d\tW\t%d\n", current->pid, jiffies); } current->state = TASK_INTERRUPTIBLE; schedule(); return 0; } // 在sleep_on()函数中 void sleep_on(struct task_struct **p) { struct task_struct *tmp; if (!p) return; if (current == &(init_task.task)) { panic("task[0] trying to sleep"); } tmp = *p; *p = current; current->state = TASK_UNINTERRUPTIBLE; fprintk(3, "%d\tW\t%d\n", current->pid, jiffies); schedule(); if (tmp) { tmp->state = 0; fprintk(3, "%d\tJ\t%d\n", tmp->pid, jiffies); } }3.2 进程退出处理
在exit.c中,我们需要追踪进程的退出:
int do_exit(long code) { // 释放资源代码... current->state = TASK_ZOMBIE; fprintk(3, "%ld\tE\t%ld\n", current->pid, jiffies); schedule(); return (-1); }4. 日志分析与可视化
收集到日志数据后,我们需要一个工具来分析这些数据。下面是一个Python脚本示例,可以解析日志文件并生成可视化图表:
#!/usr/bin/env python # stat_log.py import sys import matplotlib.pyplot as plt def parse_log_file(filename): processes = {} with open(filename, 'r') as f: for line in f: pid, state, time = line.strip().split('\t') pid = int(pid) time = int(time) if pid not in processes: processes[pid] = [] processes[pid].append((state, time)) return processes def plot_process_states(processes, pids=None): if pids is None: pids = processes.keys() fig, ax = plt.subplots(figsize=(12, 6)) for pid in pids: if pid not in processes: continue states = processes[pid] prev_time = states[0][1] prev_state = states[0][0] for state, time in states[1:]: # 绘制水平线段表示状态持续时间 ax.hlines(y=pid, xmin=prev_time, xmax=time, color=get_state_color(prev_state), linewidth=2) prev_time = time prev_state = state ax.set_yticks(list(pids)) ax.set_yticklabels([f'Process {pid}' for pid in pids]) ax.set_xlabel('Time (jiffies)') ax.set_title('Process State Transition Diagram') # 添加图例 from matplotlib.lines import Line2D legend_elements = [ Line2D([0], [0], color=get_state_color('N'), lw=2, label='New'), Line2D([0], [0], color=get_state_color('J'), lw=2, label='Ready'), Line2D([0], [0], color=get_state_color('R'), lw=2, label='Running'), Line2D([0], [0], color=get_state_color('W'), lw=2, label='Waiting'), Line2D([0], [0], color=get_state_color('E'), lw=2, label='Exit') ] ax.legend(handles=legend_elements, loc='upper right') plt.tight_layout() plt.show() def get_state_color(state): colors = { 'N': 'blue', 'J': 'green', 'R': 'red', 'W': 'orange', 'E': 'black' } return colors.get(state, 'gray') if __name__ == '__main__': if len(sys.argv) < 2: print("Usage: python stat_log.py <logfile> [pid1 pid2 ...] [-g]") sys.exit(1) filename = sys.argv[1] pids = [] show_graph = False for arg in sys.argv[2:]: if arg == '-g': show_graph = True else: try: pids.append(int(arg)) except ValueError: pass processes = parse_log_file(filename) if not pids: pids = sorted(processes.keys()) if show_graph: plot_process_states(processes, pids) else: # 文本统计输出 for pid in pids: if pid in processes: print(f"\nProcess {pid} state transitions:") for state, time in processes[pid]: print(f"{time}\t{state}")这个脚本提供了两种输出方式:
- 文本统计:显示每个进程的状态转换时间点
- 图形化展示:用不同颜色的线段表示不同状态持续时间
要运行脚本分析日志,可以使用以下命令:
python stat_log.py process.log 1 2 3 -g # 分析进程1、2、3并显示图形 python stat_log.py process.log -g # 分析所有进程并显示图形5. 时间片调整实验
Linux 0.11使用简单的时间片轮转调度算法,我们可以通过修改时间片大小来观察其对系统性能的影响。
5.1 修改时间片参数
时间片大小由priority值决定,在linux-0.11/include/linux/sched.h中定义:
#define INIT_TASK \ { 0,15,15, /* state, counter, priority */ \将第二个和第三个值(counter和priority)从15改为其他值(如10或5)即可调整时间片大小。
5.2 实验结果分析
下表展示了不同时间片设置下的调度情况对比:
| 时间片大小 | 上下文切换次数 | 平均等待时间 | 吞吐量 |
|---|---|---|---|
| 5 | 高 | 较短 | 中等 |
| 10 | 中等 | 中等 | 最高 |
| 15 | 低 | 较长 | 中等 |
从实验结果可以看出:
- 小时间片(5):导致频繁的上下文切换,虽然响应速度快,但吞吐量不是最优
- 中等时间片(10):在上下文切换开销和CPU利用率之间取得平衡,吞吐量最高
- 大时间片(15):减少了上下文切换,但可能导致交互式进程响应变慢
在实际系统调优中,需要根据工作负载特性选择合适的时间片大小。对于混合型负载(既有CPU密集型又有IO密集型进程),中等大小的时间片通常能提供最佳的整体性能。