开坑
课程:MIT 6.S081
lab:xv6
目标:掌握os,尽量手搓一个demo


Chapter 0

Note

shell维护三个文件描述符,1,0,2

进程维护文件描述符表

read系统调用会前进文件的偏移量,返回0作为文件结尾

2 >& 1:将标准错误流重定向到标准输出流

管道是一对读取和写入的文件描述符,shell使用fork来产生子进程,新的cmd来运行从管道的读描述符来获取数据

文件描述符偏移量相同的两种情况:dup调用和fork出子进程

close释放文件描述符

exec来重定向文件描述符,一个进程执行新进程时,替换掉的是进程的内存映像,但是维护的文件描述符表不会发生改变

若子进程不在退出前关闭文件描述符,父进程就会在子进程偏移的地方继续写,所以子进程的文件偏移后的文件描述符的位置会保留给父进程

当进程试图访问设备文件时,内核会将对于设备文件的读写操作转发给相应的设备驱动程序或者内核代码,而不是用文件系统来操作

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]);
}

notice:重定向是进程级别而不是系统级别
p[0]是读端口,读端口绑定到标准输入(通过dup(p[0])),其他父子进程从标准输入读取数据时实际会从读端口读取

Lab1:

父子进程间的管道通信

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main(int argc,char *argv[]){
if (argc > 1)
{
printf("meaningless parameter\n");
exit(1);
}
int pid;
int p1[2];
int p2[2];
char byte;
pipe(p1);
pipe(p2);

pid = fork(); //fork复制之后,子进程会复制所有管道,所以父进程中用不到的就可以关闭了
if (pid > 0)
{
close(p1[0]);
close(p2[1]);
if(write(p1[1],"1",1) != 1){
printf("error parent send\n");
exit(1);
}
if (read(p2[0], &byte, 1) == 1)
{
printf("%d:recieve pong\n", getpid());
}
else
{
printf("error parent recieve\n");
close(p1[0]);
close(p2[1]);
exit(1);
}
close(p1[0]);
close(p2[1]);

}
else if (pid == 0)
{
close(p1[1]);
close(p2[0]);
if(read(p1[0],&byte,1) == 1){
printf("%d:recieved ping\n", getpid());
if(write(p2[1], "1", 1) != 1){
printf("error child send\n");
exit(1);
}
exit(0);
}
else
{
printf("error child recieve\n");
exit(1);
}
}
else
{
printf("fork error");
exit(1);
}
}

notice:父子进程不同步,无法保证父进程发送数据时,子进程已经准备好接收数据,出现条件竞争,所以要使用同步机制来保证

管道实现素数筛

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

#define INDEX_READ 0
#define INDEX_WRITE 1
#define MAX 35

// 思路:对于数字的输出和处理都在子进程中完成,关闭不需要的读端口,直到所有端口全部关闭,前后有固定关系,考虑递归
// 父进程的行为和子进程对孙进程的不同
// 管道是一个有缓冲的队列!!!!

void child_read(int pipelines_ptc[]){
int pid;
int pipelines_ctgc[2];
int num;
pipe(pipelines_ctgc);
close(pipelines_ptc[INDEX_WRITE]);
// 打印初始值,并丢掉
if (read(pipelines_ptc[INDEX_READ], &num, sizeof(num)) > 0)
{
printf("prime %d\n", num);
}
else{
close(pipelines_ctgc[INDEX_READ]);
close(pipelines_ctgc[INDEX_WRITE]);
close(pipelines_ptc[INDEX_READ]);
close(pipelines_ptc[INDEX_WRITE]);
exit(0);
}

// 子进程与孙进程
if ((pid = fork()) > 0)
{
printf("child process\n");
// 子进程
close(pipelines_ctgc[INDEX_READ]);
printf("close success!\n");
int each_num;
while (read(pipelines_ptc[INDEX_READ], &each_num, sizeof(each_num)) > 0)
{
printf("Read success!\n");
if (each_num % num != 0)
{
write(pipelines_ctgc[INDEX_WRITE], &each_num, sizeof(each_num));
printf("write success!\n");
}
}
close(pipelines_ctgc[INDEX_WRITE]);
close(pipelines_ptc[INDEX_READ]);
wait(0);
}
else{
// 孙进程
child_read(pipelines_ctgc);
}

}

int main(int argc, char *argv[]){
int pipelines_ptc[2];
int pid;
pipe(pipelines_ptc);
if ((pid = fork()) > 0)
{
//父进程
close(pipelines_ptc[INDEX_READ]);
for (int i = 2; i < MAX; i++)
{
write(pipelines_ptc[INDEX_WRITE], &i, sizeof(i));
wait(0); //等待子进程完成
}
}
else{
// 子进程
child_read(pipelines_ptc);
}
exit(0);
}

find 命令实现

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"


// 递归遍历所有目录来寻找文件并打印,判断是否有文件夹,没有返回
// 接收文件名和寻找开始的路径
// 获取路径字符串的文件名
char*
fmtname(char *path)
{
static char buf[DIRSIZ+1];
char *p;

// Find first character after last slash.
for(p=path+strlen(path); p >= path && *p != '/'; p--)
;
p++;

// Return blank-padded name.
if(strlen(p) >= DIRSIZ)
return p;
memmove(buf, p, strlen(p));
memset(buf+strlen(p), ' ', DIRSIZ-strlen(p));
return buf;
}

void find(char *path, char *name){
char buf[512];
char *p;
int fd;
struct dirent de;
struct stat st;

if((fd = open(path, 0)) < 0){
fprintf(2, "find: cannot open %s\n", path);
return;
}

if(fstat(fd, &st) < 0){
fprintf(2, "ls: cannot stat %s\n", path);
close(fd);
return;
}

switch(st.type){
// 寻找的目录是文件
case T_FILE:
if (strcmp(fmtname(path), name) == 0)
{
printf("%s", path);
return;
}
break;
case T_DIR:
if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
printf("find: path too long\n");
break;
}
strcpy(buf, path);
p = buf+strlen(buf);
*p++ = '/';
while (read(fd, &de, sizeof(de)) == sizeof(de))
{
if (de.inum == 0 || !strcmp(de.name, ".") || !strcmp(de.name, ".."))
continue;

memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0; // ?
// 目录当文件处理
if (!strcmp(de.name, name))
{
printf("%s\n", buf);
continue;
}

if(stat(buf, &st) < 0){
printf("find: cannot stat %s\n", buf);
continue;
}

// 不用管文件的情况
switch (st.type)
{
case T_DIR:
find(buf, name);
break;
default:
break;
}
}
close(fd);
return;
}
close(fd);
return;
}

int main(int argc, char *argv[]){
if (argc < 3)
{
printf("Usage: 'find dir filename'\n");
exit(1);
}
for (int i = 2; i < argc; i++)
{
find(argv[1], argv[i]);
}
exit(0);
}

xargs 命令实现

  • 对于管道连接的命令,以find . b | xargs grep hello为例,argv[0]是 xargs,argv[1]是grep,一共有三个参数,如果想要读取find . b,那么需要从标准输入中读取,也即是使用read函数读取fd为0时的数据

  • 每一次读取一行,将该行所有空格替换为\0,这样命令就可以被分割。然后将argv[]指向这些命令。如果遇到换行符,执行fork,父进程等待子进程结束

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/param.h"

#define STDIN 0

int main(int argc,char *argv[])
{
char buf[1024];
char c;
char *Argv[MAXARG];
int index=0;
int i;

for(i=1;i<=argc-1;i++)
{
Argv[i-1]=argv[i];//ignore xargs(argv[0])
}

while(1)
{
index=0;
memset(buf,0,sizeof(buf));
char *p=buf;
i=argc-1;//注意i要写在这里
while(1)
{
int num=read(STDIN,&c,1);//读取标准输入,注意是&c
if(num!=1)
exit(0);//程序的终止条件
if(c==' '||c=='\n')
{
buf[index++]='\0';
Argv[i++]=p;//参数
p=&buf[index];//更新参数首地址
if(c=='\n')
break;
}
else //character
{
buf[index++]=c;
}
}
Argv[i]=0;
int pid = fork();
if(pid==0)
{
exec(Argv[0],Argv);
}
else
{
wait(0);
}
}
exit(0);
}

无缓冲通道CSP线程

CSP并发编程是一种进程间的通信方式,不需要两个进程共享状态,而是通过消息传递的方式来进行通信,可以解决死锁和条件竞争的问题,其中使用管道(pipeline)的无缓冲I/O是一种常见的来实现同步的方法:

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
package main

import (
"fmt"
"time"
)

func worker(ch chan bool) {
fmt.Println("Worker: Waiting for task...")
<-ch // 从通道接收数据,这里会阻塞直到数据发送方准备好
fmt.Println("Worker: Performing task...")
time.Sleep(2 * time.Second) // 模拟工作
fmt.Println("Worker: Task completed!")
}

func main() {
ch := make(chan bool) // 创建一个无缓冲通道

go worker(ch) // 启动一个goroutine执行worker函数

time.Sleep(1 * time.Second) // 等待一会儿

fmt.Println("Main: Sending task...")
ch <- true // 向通道发送数据,这里会阻塞直到接收方准备好
fmt.Println("Main: Task sent!")

time.Sleep(3 * time.Second) // 等待工作完成
}

Chapter 1

Note

将操作系统作为库调用会带来一系列问题
操作系统将磁盘管理抽象成文件系统,并且提供了对文件系统的接口(系统调用),来实现对应用程序访问存储的隔离
隔离的必要性:物理资源组织,异常处理和安全性
隔离方案:multiplexing,CPU分时复用
fork等操作系统接口抽象出可以和CPU交互的进程,进程在和应用程序交互,所以应用程序才能复用CPU

为什么虚拟地址能保证应用程序不直接访问物理地址的安全性?

  • 虚拟地址提供了对物理地址的抽象和扩展,便于操作系统对进程进行隔离
  • 虚拟地址可以设置一些针对物理地址的权限,并且内存管理单元MMU(用于根据页表将虚拟地址映射为虚拟地址)也可以对访问进行拦截和保护保证

exec系统调用是unix接口,将应用程序的内存镜像放到进程内存里