Operating_Systems/Lab/Lab3/source/21281280-柯劲帆-第3次实验.md
2024-09-05 13:31:02 +08:00

297 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<h1><center>北京交通大学实验报告</center></h1>
<div style="text-align: center;">
<div><span style="display: inline-block; width: 65px; text-align: center;">课程名称</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">操作系统</span></div>
<div><span style="display: inline-block; width: 65px; text-align: center;">实验题目</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">Linux进程控制的实现机制</span></div>
<div><span style="display: inline-block; width: 65px; text-align: center;">学号</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">21281280</span></div>
<div><span style="display: inline-block; width: 65px; text-align: center;">姓名</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">柯劲帆</span></div>
<div><span style="display: inline-block; width: 65px; text-align: center;">班级</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">物联网2101班</span></div>
<div><span style="display: inline-block; width: 65px; text-align: center;">指导老师</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">何永忠</span></div>
<div><span style="display: inline-block; width: 65px; text-align: center;">报告日期</span><span style="display: inline-block; width: 25px;">:</span><span style="display: inline-block; width: 210px; font-weight: bold; text-align: left;">2023年11月6日</span></div>
</div>
---
## 目录
[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`