北京交通大学实验报告

课程名称:操作系统
实验题目:Linux进程控制的实现机制
学号:21281280
姓名:柯劲帆
班级:物联网2101班
指导老师:何永忠
报告日期:2023年11月6日
--- ## 目录 [TOC] --- # 1. 开发运行环境和工具 - **代码阅读环境**:Windows11 + VS Code - **开发测试环境**:Bochs # 2. 实验过程、分析和结论 ## 2.1. fork系统调用和内核函数 首先在`init/main.c`-->`main()`中看到了`fork()`函数 ```c if (!fork()) { /* we count on this going ok */ init(); } ``` 即为如果`fork()`没有发现父进程(后面会讲到),则初始化系统,创建系统初始进程。 追踪`fork()`,发现在`main.c`开头有一行: ```c static inline _syscall0(int,fork) ``` 即调用`fork()`时,编译器会将`fork()`内嵌为`_syscall0(int,fork)`,即在编译时直接替换为`_syscall0(int,fork)`的函数代码,以免调用堆栈。 追踪`_syscall0()`,在`include/unistd.h`中: ```asm #define _syscall0(type,name) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name)); \ if (__res >= 0) \ return (type) __res; \ errno = -__res; \ return -1; \ } ``` 即,调用`fork()`就是在调用 ```asm "int $0x80" : "=a" (__res) : "0" (__NR_fork) ``` - `"int $0x80"`:表示触发软中断。在x86体系结构中,软中断0x80用于进入内核态进行系统调用。 - `: "=a" (__res)`:对输出操作数的约束。`=a` 表示将寄存器 `eax` 的值分配给 `__res`,并且这个值会在系统调用结束后存储系统调用的返回值。 - `: "0" (__NR_fork)`:对输入操作数的约束。`0` 表示使用与前面输出操作数相同的寄存器,这里是 `eax`。`__NR_fork` 是系统调用号的宏,被定义为`2`,用于标识调用的具体系统调用。 那么调用`fork()`时,就是调用了2号系统软中断。 接下来到`kernel/system_call.s`中查看软中断函数`_system_call`代码是如何执行的:首先进行了一系列参数的检查和原寄存器入栈保存操作,这里不予赘述。然后执行了 ```asm call _sys_call_table(,%eax,4) ``` 按照传进来的参数,实际上调用了`_sys_call_table(,2,4)` 由于这是汇编文件,调用`_sys_call_table()`实际上就是在调用C语言代码中的`sys_call_table()`函数,因为在编译时,编译器会在C代码函数前加上`_`作为其在汇编代码中的命名(实验证明见附录)。 那么追踪`sys_call_table()`,在`include/linux/sys.h`中发现了系统调用表: ```c fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read, sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link, sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod, sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount, sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm, sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access, sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir, sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid, sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys, sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit, sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid, sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask, sys_setreuid,sys_setregid }; ``` 其中变量类型`fn_ptr`是函数指针。所以实际上`_sys_call_table(,2,4)`调用了第3个`sys_fork`函数。`sys_fork`函数在汇编后会被命名成`_sys_fork`。 我在`kernel/system_call.s`中找到了对其汇编后的调用`_sys_fork`,即系统调用`fork`的函数原型就是: ```asm _sys_fork: call _find_empty_process testl %eax,%eax js 1f push %gs pushl %esi pushl %edi pushl %ebp pushl %eax call _copy_process addl $20,%esp 1: ret ``` 其中依次调用了`_find_empty_process`和`_copy_process`函数。这两个函数的C代码`find_empty_process()`和`copy_process()`在`kernel/fork.c`中: - `find_empty_process()`:寻找一个空闲的pid,并为其在任务数组中为新任务寻找一个空闲项,并返回项号。 - `copy_process()`:复制父进程的各项值并初始化(这么做是因为复制比新建快)。 在这之中,代码为新进程创建了进程控制块`struct task_struct *p`,到`include/linux/sched.h`中跟踪其定义: ```c struct task_struct { /* these are hardcoded - don't touch */ long state; /* -1 unrunnable, 0 runnable, >0 stopped */ // 表示进程的状态,-1 表示不可运行,0 表示可运行,大于0 表示已停止。 long counter; // 计时器,用于调度,当计时器减至0时,进程可能被调度出去。 long priority; // 进程的优先级。 long signal; // 当前进程正在处理的信号。 struct sigaction sigaction[32]; // 存储信号处理程序的数组。 long blocked; /* bitmap of masked signals */ // 用于表示被阻塞的信号的位图。 /* various fields */ int exit_code; unsigned long start_code,end_code,end_data,brk,start_stack; long pid,father,pgrp,session,leader; // pid 进程ID // father 父进程ID // pgrp 进程组ID // session 会话ID // leader 会话的领导者ID unsigned short uid,euid,suid; // 用户ID、有效用户ID、保存的用户ID unsigned short gid,egid,sgid; // 组ID、有效组ID、保存的组ID long alarm; // 进程的闹钟定时器 long utime,stime,cutime,cstime,start_time; // utime 用户态运行时间 // stime 系统态运行时间 // cutime 子进程的用户态运行时间 // cstime 子进程的系统态运行时间 // start_time 进程开始运行的时间 unsigned short used_math; // 表示是否使用了数学协处理器 /* file system info */ int tty; // 表示进程是否有终端 /* -1 if no tty, so it must be signed */ unsigned short umask; // 文件创建的权限屏蔽掩码 struct m_inode * pwd; // 当前工作目录 struct m_inode * root; // 当前根目录 struct m_inode * executable; // 执行文件的指针 unsigned long close_on_exec; // 用于在执行新程序时关闭文件的标志位 struct file * filp[NR_OPEN]; // 文件指针数组,用于表示打开的文件 /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */ struct desc_struct ldt[3]; // 进程的局部描述符表,包含代码段、数据段、堆栈段的描述符 /* tss for this task */ struct tss_struct tss; // 任务状态段,包含了一些处理器的状态信息 }; ``` 最后,返回新建进程的pid。 于是,`fork`调用就成功创建了新的进程和pid,并把pid返回给了调用`fork`的进程。 ## 2.2. exit系统调用和内核函数 首先在`init/main.c`-->`main()`中看到了`_exit()`函数: ```c if (!(pid=fork())) { close(0); if (open("/etc/rc",O_RDONLY,0)) _exit(1); execve("/bin/sh",argv_rc,envp_rc); _exit(2); } ``` 追踪`_exit()`,到`lib/_exit.c`中看到定义: ```c volatile void _exit(int exit_code) { __asm__("int $0x80"::"a" (__NR_exit),"b" (exit_code)); } ``` 和`fork()`一样,`_exit()`也是使用了系统中断来执行。 `__NR_exit`的宏定义为系统调用号1,所以实际上:`__NR_exit` 的值被加载到 `%eax` 寄存器;`exit_code` 的值被加载到 `%ebx` 寄存器,作为 `exit` 系统调用的参数,即退出码。 于是在`kernel/system_call.s`中,软中断函数`_system_call`代码执行了`call _sys_call_table(,1,4)`,即调用了系统调用表中的`sys_exit`。 `sys_exit`的定义在`kernel/exit.c`中: ```c int sys_exit(int error_code) { return do_exit((error_code&0xff)<<8); } ``` 再追踪`do_exit()`的代码,主要执行以下任务: - 释放当前进程的页表 - 处理与当前进程相关的其他进程 - 关闭当前进程打开的文件 - 释放当前进程的pwd、root和executable相关资源 - 处理当前进程是会话领导者等情况 - 通知父进程 - 调度下一个进程 至此,本进程结束退出,exit系统调用完毕。 ## 2.3. 原子性实现 纵观代码,进程间的切换主要有两种方式,一种是主动切换,如`pause()`,其作用就是主动将当前进程挂起,切换到下一个进程任务执行,需要调用`schedule()`。在`schedule()`中会判断要操作的进程的状态是否是可中断的。 另一种是通过开关中断,主动允许/不允许中断打断当前进程。在`include/asm/system.h`中: ```c #define sti() __asm__ ("sti"::) #define cli() __asm__ ("cli"::) ``` 因此,在代码中使用`sti()`和`cli()`就能实现开/关中断。 在`fork`和`exit`系统调用代码中,并没有调用`pause()`主动切换,也不会因为硬件中断而导致共享资源出错(对栈的使用得当),因此实现了原子性。 ## 2.4. 修改fork系统调用 修改`kernel/fork.c`文件中`copy_process()`函数定义,在`return`前打印出新进程的pid和状态: ```c p->state = TASK_RUNNING; /* do this last, just in case */ printk("pid=%d, state=%d", last_pid, p->state); return last_pid; ``` 运行结果如下: ![p1](p1.png) `state`为0表示当前进程(在刚刚创建时)是就绪态和运行态。 ## 2.5. Makefile文件 首先,文件定义了编译必要的工具和选项,以及根设备、目标文件等; 接下来定义了编译规则,如从`.c`文件编译出`.s`文件等; 然后定义最后生成`Image`镜像文件; 规定`Image`文件的构建规则、写入磁盘规则; 定义构建工具的规则; 定义编译完后要清理的文件; 创建备份; 规定项目源码文件之间构建的依赖关系。 # 附录 编译`kernel/fork.c`: ```sh [usr/src/linux/kernel]# gcc -E fork.c -fork.i [usr/src/linux/kernel]# gcc -S fork.i -fork.s [usr/src/linux/kernel]# vi fork.s ``` 则可以看到,函数`verify_area()`变成了`_verify_area`,函数`copy_mem()`变成了`_copy_mem`。