MIT6S081

操作系统再学习

资源

MIT 6.S081: Operating System Engineering - CS自学指南 (csdiy.wiki)

第零章 操作系统接口 | xv6 中文文档 (gitbooks.io)

course

evn

https://pdos.csail.mit.edu/6.S081/2022/labs/util.html

我在我滴腾讯云上搞的 怕把本地环境搞的乱七八糟,,,

LEC 1

进程和内存

1
2
3
4
5
6
7
8
9
10
11
12
int pid;
pid = fork();
if(pid > 0){
printf("parent: child=%d\n", pid);
pid = wait();
printf("child %d is done\n", pid);
} else if(pid == 0){
printf("child: exiting\n");
exit();
} else {
printf("fork error\n");
}

这里有个疑惑

1
2
parent: child=1234
child: exiting

为什么这两个会没有顺序被打印出来?

Q: 我有一个子进程 应该都过了 pid>0 然后不是应该先打 parent?

我的我的 太长时间没学 变傻了

A: 执行顺序是这样的 但打印的时间是个比较玄幻的东西 主要看谁先打印完?

I/O 和文件描述符

PIPE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p);
if(fork() == 0) {
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
exec("/bin/wc", argv);
} else {
write(p[1], "hello world\n", 12);
close(p[0]);
close(p[1]);
}

HW

实现sleep

建议

  • Before you start coding, read Chapter 1 of the xv6 book.
  • Look at some of the other programs in user/ (e.g., user/echo.c, user/grep.c, and user/rm.c) to see how you can obtain the command-line arguments passed to a program.
  • If the user forgets to pass an argument, sleep should print an error message.
  • The command-line argument is passed as a string; you can convert it to an integer using atoi (see user/ulib.c).
1
2
3
4
5
6
7
8
9
10
int
atoi(const char *s)
{
int n;

n = 0;
while('0' <= *s && *s <= '9')
n = n*10 + *s++ - '0';
return n;
}
  • Use the system call sleep.
  • See kernel/sysproc.c for the xv6 kernel code that implements the sleep system call (look for sys_sleep), user/user.h for the C definition of sleep callable from a user program, and user/usys.S for the assembler code that jumps from user code into the kernel for sleep.
  • Make sure main calls exit() in order to exit your program.
  • Add your sleep program to UPROGS in Makefile; once you’ve done that, make qemu will compile your program and you’ll be able to run it from the xv6 shell.
  • Look at Kernighan and Ritchie’s book The C programming language (second edition) (K&R) to learn about C.

稀里糊涂看了下 chapter 1 , echo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
int i;

for(i = 1; i < argc; i++){
write(1, argv[i], strlen(argv[i]));
if(i + 1 < argc){
write(1, " ", 1);
} else {
write(1, "\n", 1);
}
}
exit(0);
}

成果

首先大家都用了这三个库 我们也用

然后需要考虑一下 这个参数改怎么写?

他的要求是

  • 没输入参数要报错
  • 传入一个 string,然后我们给他转成int类型, 然后该停多少秒是多少秒
  • 使用系统调用sleep?

https://blog.csdn.net/zhaozhiyuan111/article/details/104050729 参考 argc argv

说实话 不是很会调试c,,,

哦哦 忘了改makelist 了 所以没打开,,,

。。。

最后改成这样 但是不知道为什么 他并没有sleep o.o… 好像是我的理解有点问题

这个 const 有点小难受,,,没太搞懂

ps: 还要抽时间写个 exit 我这个快捷键不知道为什么用不了,,,服了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char * argv[]){

if(argc >= 1){
int n;
n = atoi(argv[1]);
printf("%d",n);
sleep(n);
return 0;
}else if(argc == 0){
printf("error");
return 0;
}
return 0;
}

不知道为什么 传进去的还是char 而不是const char

难道是因为我的const 加到了 数组上去?

难受啊 https://stackoverflow.com/questions/1143262/what-is-the-difference-between-const-int-const-int-const-and-int-const?rq=1

。。。

用法错误 但我的代码为什么不sleep??????

没办法接着往后学了 说不定哪天回来看能解决(ps: 跑别人写的也不sleep

最后发现问题了 是时间单位的问题,,,

实现pingpong

https://blog.csdn.net/skyroben/article/details/71513385

写的很好

先整个小例子试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(){
int pid = fork();
if(pid > 0){
printf("parent: child=%d\n", pid);
pid = wait((int *) 0);
printf("child %d is done\n", pid);
} else if(pid == 0){
printf("child: exiting\n");
exit(0);
} else {
printf("fork error\n");
}

return 0;
}

我的理解是 父进程中 pid 为 fork 生成的子进程的进程号 而子进程虽然fork了pid 但是值为0

简单写了个半成品 但出了点问题

fd[0] 是读 fd[1] 是写

他的要求是 父进程 先给子进程send byte 子进程接着要给父进程send byte

想了很久 确实需要两条通道? 先记录一下失败的案例 写着写着就乱了,,,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int 
main(){
int fd[2];
int ret = pipe(fd);
if(ret == -1){
printf("pipe error\n");
}
int pid = fork();
if(pid > 0){ // father
close(fd[1]);
char *bytes = "hi!";
write(fd[0],bytes,strlen(bytes)+1);
printf("%d: received pong\n",getpid());


} else if(pid == 0){ // child
close(fd[0]);
char msg[100];
memset(msg,'\0',sizeof(msg));
int s = read(fd[0],msg,sizeof(msg));
if (s>0){
msg[s-1] = '\0';
}
printf("%s\n",msg);
printf("%d: received ping\n",getpid());

} else {
printf("fork error\n");
}

return 0;

}

正确的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(){
int fd[2]; //fd[0] 是读 fd[1] 是写
int ret = pipe(fd);
if(ret == -1){
printf("pipe error\n");
}
char buf[2];

char *msg1 = "a";
char *msg2 = "b";
int pid = fork();
if(pid > 0){ // father
write(fd[1],msg1,1);
close(fd[1]); // shut fd[1]
read(fd[0],buf,1);
printf("%c\n",buf[0]);
printf("%d: received pong\n",getpid());


} else if(pid == 0){ // child

read(fd[0],buf,1); // read from father

printf("%c\n",buf[0]);
printf("%d: received ping\n",getpid());
close(fd[0]);
write(fd[1],msg2,1);

} else {
printf("fork error\n");
}

return 0;

}

LEC 3

隔离性

如果没有操作系统,应用程序会直接与硬件交互

这并不是一个很好的设计。这里你可以看到这种设计是如何破坏隔离性的。使用操作系统的一个目的是为了同时运行多个应用程序,所以时不时的,CPU会从一个应用程序切换到另一个应用程序。我们假设硬件资源里只有一个CPU核,并且我们现在在这个CPU核上运行Shell。但是时不时的,也需要让其他的应用程序也可以运行。现在我们没有操作系统来帮我们完成切换,所以Shell就需要时不时的释放CPU资源。

进程本身不是CPU,但是它们对应了CPU,它们使得你可以在CPU上运行计算任务。所以你懂的,应用程序不能直接与CPU交互,只能与进程交互。

学生提问:这里说进程抽象了CPU,是不是说一个进程使用了部分的CPU,另一个进程使用了CPU的另一部分?这里CPU和进程的关系是什么?

Frans教授:我这里真实的意思是,我们在实验中使用的RISC-V处理器实际上是有4个核。所以你可以同时运行4个进程,一个进程占用一个核。但是假设你有8个应用程序,操作系统会分时复用这些CPU核,比如说对于一个进程运行100毫秒,之后内核会停止运行并将那个进程从CPU中卸载,再加载另一个应用程序并再运行100毫秒。通过这种方式使得每一个应用程序都不会连续运行超过100毫秒。这里只是一些基本概念,我们在接下来的几节课中会具体的看这里是如何实现的。

学生提问:好的,但是多个进程不能在同一时间使用同一个CPU核,对吧?

Frans教授:是的,这里是分时复用。CPU运行一个进程一段时间,再运行另一个进程。

防御性

这里的硬件支持包括了两部分,第一部分是user/kernel mode,kernel mode在RISC-V中被称为Supervisor mode但是其实是同一个东西;第二部分是page table或者虚拟内存(Virtual Memory)。

实际上RISC-V还有第三种模式称为machine mode。在大多数场景下,我们会忽略这种模式,所以我也不太会介绍这种模式。 所以实际上我们有三级权限(user/kernel/machine),而不是两级(user/kernel)。

每一个进程都会有自己独立的page table,这样的话,每一个进程只能访问出现在自己page table中的物理内存。操作系统会设置page table,使得每一个进程都有不重合的物理内存,这样一个进程就不能访问其他进程的物理内存,因为其他进程的物理内存都不在它的page table中。一个进程甚至都不能随意编造一个内存地址,然后通过这个内存地址来访问其他进程的物理内存。这样就给了我们内存的强隔离性。

User/Kernel mode切换

说的就是一个东西 将控制权从应用程序转到操作系统

例子如下

假设我现在要执行另一个系统调用write,相应的流程是类似的,write系统调用不能直接调用内核中的write代码,而是由封装好的系统调用函数执行ECALL指令。所以write函数实际上调用的是ECALL指令,指令的参数是代表了write系统调用的数字。之后控制权到了syscall函数,syscall会实际调用write系统调用。

编译运行kernel

首先,Makefile(XV6目录下的文件)会读取一个C文件,例如proc.c;之后调用gcc编译器,生成一个文件叫做proc.s,这是RISC-V 汇编语言文件;之后再走到汇编解释器,生成proc.o,这是汇编语言的二进制格式。

之后,系统加载器(Loader)会收集所有的.o文件,将它们链接在一起,并生成内核文件。

XV6 启动过程

还要回来重点学一下

HW

Lab: System calls (mit.edu)

System call tracing

  • Add $U/_trace to UPROGS in Makefile
  • Run make qemu and you will see that the compiler cannot compile user/trace.c, because the user-space stubs for the system call don’t exist yet: add a prototype for the system call to user/user.h, a stub to user/usys.pl, and a syscall number to kernel/syscall.h. The Makefile invokes the perl script user/usys.pl, which produces user/usys.S, the actual system call stubs, which use the RISC-V ecall instruction to transition to the kernel. Once you fix the compilation issues, run trace 32 grep hello README; it will fail because you haven’t implemented the system call in the kernel yet.
  • Add a sys_trace() function in kernel/sysproc.c that implements the new system call by remembering its argument in a new variable in the proc structure (see kernel/proc.h). The functions to retrieve system call arguments from user space are in kernel/syscall.c, and you can see examples of their use in kernel/sysproc.c.
  • Modify fork() (see kernel/proc.c) to copy the trace mask from the parent to the child process.
  • Modify the syscall() function in kernel/syscall.c to print the trace output. You will need to add an array of syscall names to index into.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
int i;
char *nargv[MAXARG];

if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){
fprintf(2, "Usage: %s mask command\n", argv[0]);
exit(1);
}

if (trace(atoi(argv[1])) < 0) {
fprintf(2, "%s: trace failed\n", argv[0]);
exit(1);
}

for(i = 2; i < argc && i < MAXARG; i++){
nargv[i-2] = argv[i];
}
exec(nargv[0], nargv);
exit(0);
}

在三个文件中加上东西后 就可以成功编译了

但是

1
2
3
$ trace 32 grep hello README
10 trace: unknown sys call 22
trace: trace failed

并没有实现系统调用

sysinfo

要求实现

1
2
3
4
struct sysinfo {
uint64 freemem; // amount of free memory (bytes)
uint64 nproc; // number of process
};

实现两个函数

  • To collect the amount of free memory, add a function to kernel/kalloc.c
  • To collect the number of processes, add a function to kernel/proc.c

GDB调试

1
make qemu-gdb

再 主要是用了这个工具 gdb-multiarch

1
2
gdb-multiarch
(gdb) target remote localhost:xxxx

即可

GDB禁用和删除断点 (biancheng.net)

用的不是很熟,,不好玩

LEC 4

虚拟内存基本思想:

每个程序拥有自己的地址空间,这个空间被分割成多个块,每一块称作一页或页面(page)。每一页拥有连续的地址范围。

之前得知了几个启动时会调用的函数

kvminit:设置好虚拟内存

kvminithart:打开页表

LEC 9

就是硬件想要得到操作系统的关注。例如网卡收到了一个packet,网卡会生成一个中断;用户通过键盘按下了一个按键,键盘会产生一个中断。