MIT 6.S081入门lab1 操作系统及其接口
一、参考资料阅读与总结
1.xv6 book书籍阅读(操作系统接口)
a.总览
- 操作系统的任务: 多个程序之间共享计算机(计算机的硬件管理+任务调度)
- 操作系统接口: 使用系统调用,调用内核服务为用户端程序提供给服务(即实现对进程的调度和硬件的管理)
- 操作系统权限管理: 内核使用CPU提供的硬件保护机制(MMU)保证硬件(尤其是内存)的安全
b.进程和内存(fork、exec)
- 进程使用fork创建进程: 本质是使用pid来判断进程的状态(子进程pid为0,父进程pid > 0)。涉及函数fork
注意: 通过系统调用fork,父进程的内存和文件描述符表都会被拷贝一份给新的子进程 - exec系统调用: 本质是在读取可执行文件中的内存镜像并替换到调用进程的内存空间,其需要文件地址(需文件系统)和参数值,使用ELF格式。涉及函数exec
注意: exec完全替换调用进程的内存,但保留当前进程的文件描述符表 - shell执行程序: 其位于用户空间、通过getcmd读取输入、并使用fork创建子进程,使用parsecmd处理格式并运行runcmd执行命令,父进程原地等待子进程完成工作。
c. I/O和文件描述符(read()、write() 、close()、open()、dup())
- 文件描述符(文件): 抽象文件、管道、设备之间的差异->字节驱动程序;每个进程都有着一个从0开始的私有fd,0:标准输入、1:标准输出、2:标准错误。
- 输入输出(I/O): read()、write() 、close()、open():写入、写出、释放文件、打开文件(模式)。
- I/O重定向: 本质是fd操作和fork的相互作用
注意:子进程的文件描述符的重分配并不会影响父进程的文件描述符;每个文件描述符下对应文件的偏移量,在父进程和子进程中是共享的 - 系统调用dup(): 可以理解为fd的别名。
注意: 通过过fork或dup的系统调用,从一个相同的原始文件描述符派生出来的两个文件描述符,一定会共享同一个文件的偏移量。否则,文件描述符不会共享偏移量。
d.管道(pipe)
- 管道: 其本质是一个小的内核缓冲区,是进程间通信的其中一种方式。对于进程来说是一对文件fd,一个写入一个读出(半双工)
注意: 进程间通信IPC,有两大基本模型:共享内存和消息传递;基于共享内存模型的方式需要内核的干预较少,开销更小,速度更快;基于消息传递模型的方式则适合用于传递小规模数据,而且在分布式系统中易于实现。 - I/O重定向: 通过dup和pipe的操作可以完成对对标准I/O的重定向,同时,由于为匿名管道,因此xv6中使用pipe创建的管道只能被有亲缘的进程打开。
- 管道使用注意: 管道在使用后要及时关闭,因为管道的操作是阻塞的;0为读、1为写。
- shell中管道的应用: 在(user/sh.c:100)中我们可以看到,在获取到管道信息 | 后,shell创建子进程1,将左边命令的输出重定向为管道输入;并创建子进程2,将右边命令的输入重定向为管道输出。
- 和其他方式的对比:
与共享内存:
数据拷贝:管道方式中,数据拷贝从一个进程的用户缓冲区->内核缓冲区->内存,然后再从内存->内核缓冲区->另一个进程的用户缓冲区;而对于共享内存而言,数据拷贝时不需要经过中间的内核缓冲区。(是否需要内核缓冲区);
内部实现:管道方式中,内部是一个循环队列,可以连续传输比较大的数据;而共享内存就只是一片内存区域,每次写入的大小都是固定的。(传输模式:循环队列-内存区域);
数据读取:管道方式中,读写数据只能顺序读写,而且也不能反复读取同一数据;共享内存就没有这些限制。(读取限制:顺序<->任意);
封装性:管道方式具有完备的消息传递和通知机制;共享内存还需要搭配如互斥锁、信号量等同步工具一同使用。(管道为封装好的机制)
与临时文件:
管道自动清理相关资源(分配的内核缓冲区),而临时文件则会保留,需要另外清理。(是否自动清理)
管道可以传输任意长的字节流,文件重定向则需要磁盘上有足够空间。(空间属性:内核缓冲区<->磁盘文件)
管道左右两边的程序是可以并行执行的,而文件方式则必须等待第一个命令的完成。(同步执行:非阻塞<->阻塞)
如果你正在实现进程间通信,管道的阻塞读写会比文件的非阻塞语义更有效。
e.文件系统(chdir()、makdir()、open()、mknod()、fstat()、link()、unlink())
- Unix风格文件系统: 文件系统接口+ / 根目录+ . 当前目录+.. 上一级目录。
- 文件名称: links:文件名+inode指针;inode:文件元数据,包括文件类型、长度、磁盘位置、links数量等。
- 文件操作: mkdir():创建新目录;mknod():创建设备文件;fstat():抽取inode信息到stat中;link():创建新link;unlink():删除link[可用于与open结合创建临时文件]
- cd实现: 由于cd操作牵扯到改变进程当前目录,因此不能在子京城中处理,其被内嵌到shell中。
f.总结、xv6与真实操作系统的差异
- Unix贡献核心: 提供了一个强大的抽象,将底层的资源抽象为统一的文件
二、涉及函数
- shell.c: main->getcmd->(父进程等待)子进程fork1->parsecmd解析命令->runcmd根据类型运行命令
- ls.c:
通过fd判定stat的Type种类判定一个路径是文件还是目录;
使用读取DIRSIZ大小的字节来遍历子文件/子文件目录;
需要使用buf构造完整路径
三、课程视频观看笔记
- 操作系统的任务: 抽象硬件资源、硬件多用复用、进程隔离性、文件共享、访问控制系统、性能良好、用途广泛。
- 操作系统构成: 内核空间(文件系统、进程管理、内存管理、访问控制等)+接口()+用户空间
- API与函数的不同: API使用的是内核权限,因此可以修改硬件。
- 代码阅读:copy.c、open.c、fork.c
- 文件描述符的原理:将其索引到内核中一个维护状态进程的表中(每个进程一张表),这个表表示了每个进程中存在的文件描述符。
- (fork)父进程和子进程的异同: 有相同的代码、数据段和栈,但是地址空间不同,pid不同(xv6);注意fd是复制的
- (exec)在程序内执行外部文件: 覆盖当前进程(内存层级,可以理解为ps的覆盖+跳转),并使用命令行参数传递;注意fd是保留的
- fork和exec互动: 运行fork创建子进程,之后在子进程中使用exec执行目标程序(shell执行)。同时父进程可以使用wait查看子进程的退出状态。
- 父进程和子进程的相互等待关系: 父进程可以使用wait实现对子进程的等待并返回子进程id,而子进程无法等待父进程。
- I/O重定向技巧: 使用fork创建子进程,之后使用close+open实现对fd的IO重定向,最后使用exec执行需要的程序。
四、完成lab及其代码
sleep.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
if(argc != 2)
{
fprintf(2, "Usage: sleep <ticks>\n");
exit(1);
}
int ret = sleep(atoi(argv[1]));
exit(ret);
}
pingping.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
int pid;
char buf[]= {'m'};
int err;
// if(argc != 1)
// {
// printf("Usage: pingpong\n");
// exit(1);
// }
int pipe_P2C[2], pipe_C2P[2];
pipe(pipe_P2C);
pipe(pipe_C2P);
int ret = fork();
if (ret == 0) {
// child
pid = getpid();
err = close(pipe_P2C[1]);
if (err == -1)
{
printf("child close pipe_P2C fail\n");
}
err = close(pipe_C2P[0]);
if (err == -1)
{
printf("child close pipe_C2P fail\n");
}
err = read(pipe_P2C[0], buf, 1);
if (err == -1)
{
printf("child read pipe_P2C fail\n");
}
printf("%d: received ping\n",pid);
err = write(pipe_C2P[1], buf, 1);
if (err == -1)
{
printf("child write pipe_C2P fail\n");
}
exit(0);
}
else
{
// parent
pid = getpid();
err = close(pipe_C2P[1]);
if (err == -1)
{
printf("parent close pipe_C2P fail\n");
}
err = close(pipe_P2C[0]);
if (err == -1)
{
printf("parent close pipe_P2C fail\n");
}
err = write(pipe_P2C[1],buf, 1);
if (err == -1)
{
printf("parent write pipe_P2C fail\n");
}
err = read(pipe_C2P[0], buf, 1);
if (err == -1)
{
printf("parent read pipe_C2P fail\n");
}
printf("%d: received pong\n",pid);
exit(0);
}
}
prime.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#define READ 0
#define WRITE 1
#define STDIN 0
#define STOUT 1
#define STERR 2
#define PRIMES_NUMS 35
#define END_SIGNAL -1
int primes_process(int listenfd)
{
int ret;
int i_primeNums;
int writePipe[2]; //写管道
int buf;
read(listenfd, &i_primeNums, sizeof(i_primeNums));
if (i_primeNums == END_SIGNAL)
{
exit(0);
}
fprintf(STOUT, "prime %d\n", i_primeNums);
pipe(writePipe);
ret = fork();
if(ret == 0)
{
close(writePipe[WRITE]);
close(listenfd);
primes_process(writePipe[READ]);
exit(0);
}
else
{
close(writePipe[READ]);
while (read(listenfd, &buf, sizeof(buf)) && buf != END_SIGNAL)
{
if(buf % i_primeNums != 0)
{
write(writePipe[WRITE], &buf, sizeof(buf));
}
}
buf = -1;
write(writePipe[WRITE], &buf, sizeof(buf));
wait(0);
exit(0);
}
}
int
main(int argc, char *argv[])
{
int err;
if(argc != 1)
{
printf("Usage: primes\n");
exit(1);
}
int i;
int pipe_p[2];
err = pipe(pipe_p);
if (err == -1) {
fprintf(STERR,"pipe create fail\n");
exit(1);
}
int ret = fork();
if (ret == 0)
{
// child
close(pipe_p[WRITE]);
primes_process(pipe_p[READ]);
exit(0);
} else
{
// parent
close(pipe_p[READ]);
for ( i = 2; i <= PRIMES_NUMS; i++)
{
write(pipe_p[WRITE], &i, sizeof(i));
}
i = END_SIGNAL;
write(pipe_p[WRITE], &i, sizeof(i));
close(pipe_p[WRITE]);
wait(0);
exit(0);
}
}
find.c
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
void
find(char *path, char *target)
{
char buf[512], *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, "find: cannot stat %s\n", path);
close(fd);
return;
}
switch(st.type){
case T_FILE:
if (strcmp(path+strlen(path) - strlen(target), target) == 0)
{
printf("%s\n", path);
}
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)
continue;
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
if(stat(buf, &st) < 0){
printf("find: cannot stat %s\n", buf);
continue;
}
if(strcmp(buf+strlen(buf) - 2, "/." ) != 0 && strcmp(buf+strlen(buf) - 3, "/..") != 0)
{
find(buf, target);
}
}
break;
}
close(fd);
}
int
main(int argc, char *argv[])
{
char target[512];
if(argc != 3 )
{
printf("Usage: find [path] [target filename]\n");
exit(1);
}
target[0] = '/';
strcpy(target+1, argv[2]);
find(argv[1], target);
exit(0);
}
xarg.c
// Shell.
#include "kernel/types.h"
#include "user/user.h"
#include "kernel/fcntl.h"
#include "kernel/param.h"
#include "user/user.h"
// Parsed command representation
#define BUF_SIZE 1024
int
main(int argc, char* argv[])
{
int pid, bufIndex = 0;
char bufp, buf[BUF_SIZE+1] = {0}, *xargv[MAXARG] = {0};
if(argc < 2)
{
fprintf(2, "Useage: xargs <command> \n");
exit(1);
}
// read argv
for (int i = 1; i < argc; i++)
{
xargv[i - 1] = argv[i];
}
// read stdin
while(read(0,&bufp, 1) > 0)
{
if (bufp == '\n') {
buf[bufIndex] = '\0';
if((pid = fork()) < 0) {
fprintf(2, "fork error \n");
exit(1);
} else if ( pid == 0)
{
//child
xargv[argc - 1] = buf;
xargv[argc] = '\0';
exec(argv[1], xargv);
} else
{
//parent
wait(0);
bufIndex = 0;
}
}
else
{
buf[bufIndex++] = bufp;
}
}
exit(0);
}
参考文献:
2020版xv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf
xv6手册与代码笔记:https://zhuanlan.zhihu.com/p/350949057
xv6阅读笔记:https://ghostasky.github.io/2022/07/12/XV6/
xv6手册中文版:http://xv6.dgs.zone/tranlate_books/book-riscv-rev1/c1/s3.html
28天速通MIT 6.S081操作系统公开课:https://zhuanlan.zhihu.com/p/632281381
MIT6.s081操作系统笔记:https://juejin.cn/post/7005116617478668296