MIT 6.S081 Lab One
- 引言
- sleep(难度:Easy)
- 解析
- Lab代码实现
- pingpong(难度:Easy)
- Lab代码实习
- 小结
引言
本文为 MIT 6.S081 2020 操作系统 实验一解析。
MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列
sleep(难度:Easy)
任务:
- 实现xv6的UNIX程序sleep:您的sleep应该暂停到用户指定的计时数。
一个滴答(tick)是由xv6内核定义的时间概念,即来自定时器芯片的两个中断之间的时间。您的解决方案应该在文件user/sleep.c中
Tips:
- 在你开始编码之前,请阅读《book-riscv-rev1》的第一章
- 看看其他的一些程序(如:
/user/echo.c, /user/grep.c, /user/rm.c
)查看如何获取传递给程序的命令行参数 - 如果用户忘记传递参数,sleep应该打印一条错误信息
- 命令行参数作为字符串传递; 您可以使用atoi将其转换为数字(
详见/user/ulib.c
) - 使用系统调用sleep
- 请参阅
kernel/sysproc.c
以获取实现sleep系统调用的xv6内核代码(查找sys_sleep
),user/user.h
提供了sleep的声明以便其他程序调用,用汇编程序编写的user/usys.S
可以帮助sleep从用户区跳转到内核区。 - 确保main函数调用exit()以退出程序。
- 将你的sleep程序添加到Makefile中的UPROGS中;完成之后,make qemu将编译您的程序,并且您可以从xv6的shell运行它。
- 看看Kernighan和Ritchie编著的《C程序设计语言》(第二版)来了解C语言。
运行效果:
- 从xv6 shell运行程序:
$ make qemu
...
init: starting sh
$ sleep 10
(nothing happens for a little while)
$
- 如果程序在如上所示运行时暂停,则解决方案是正确的。运行
make grade
看看你是否真的通过了睡眠测试。 - 请注意,
make grade
运行所有测试,包括下面作业的测试。如果要对一项作业运行成绩测试,请键入(不要启动XV6,在外部终端下使用):
$ ./grade-lab-util sleep
- 这将运行与
sleep
匹配的成绩测试。或者,您可以键入:
$ make GRADEFLAGS=sleep grade
- 效果是一样的。
解析
- /user/echo.c函数代码如下:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
//argc是命令行参数个数
int main(int argc, char *argv[]){
int i;
// 依次处理每个命令行参数
for(i = 1; i < argc; i++){
// 默认情况下,文件描述符0对应标注输入,文件描述符1对应标准输出
//文件描述符2对应标准错误
write(1, argv[i], strlen(argv[i]));
//每输出一个参数,就拼接一个换行符,如果是最后一个参数了,那么拼接一个" "
if(i + 1 < argc){
write(1, " ", 1);
} else {
write(1, "\n", 1);
}
}
exit(0);
}
- 字符串转整数的atoi函数代码如下(/user/echo.c):
int atoi(const char *s){
int n=0;
while('0' <= *s && *s <= '9')
//每次处理一个字符,n每次乘10进一位,然后*s-'0'计算出当前字符代表数字几
n = n*10 + *s++ - '0';
return n;
}
- user/user.h中的sleep声明
int sleep(int);
- user/usys.S中编写的关于sleep函数的汇编实现—通过ecall指令完成系统调用
.global sleep
sleep:
li a7, SYS_sleep
ecall
ret
- syscall.h头文件中,列举出了所有支持的系统调用号
// System call numbers
#define SYS_fork 1
#define SYS_exit 2
#define SYS_wait 3
#define SYS_pipe 4
#define SYS_read 5
#define SYS_kill 6
#define SYS_exec 7
#define SYS_fstat 8
#define SYS_chdir 9
#define SYS_dup 10
#define SYS_getpid 11
#define SYS_sbrk 12
#define SYS_sleep 13
#define SYS_uptime 14
#define SYS_open 15
#define SYS_write 16
#define SYS_mknod 17
#define SYS_unlink 18
#define SYS_link 19
#define SYS_mkdir 20
#define SYS_close 21
- kernel/sysproc.c中的sys_sleep系统调用函数代码如下:
uint64
sys_sleep(void)
{
int n;
uint ticks0;
//从当前任务上下文中获取a0寄存器的值
//a0寄存器作为系统调用参数寄存器,存放sleep(int)中int参数值
if(argint(0, &n) < 0)
return -1;
//加锁
acquire(&tickslock);
//时钟中断每发生一次,ticks数加一 -- 此处是获取当前ticks数
//ticks0保存进入睡眠的ticks数
ticks0 = ticks;
//进入sleep状态
//每次都唤醒时,检查自身的sleep time是否到期,到期就停止sleep
while(ticks - ticks0 < n){
//如果进程被杀掉了,直接释放锁,然后返回-1
if(myproc()->killed){
release(&tickslock);
return -1;
}
//睡眠
sleep(&ticks, &tickslock);
}
//释放锁
release(&tickslock);
return 0;
}
- kernel/proc.c中的sleep函数代码如下:
// Atomically release lock and sleep on chan.
// Reacquires lock when awakened.
void
sleep(void *chan, struct spinlock *lk)
{
//获取当前任务上下文
struct proc *p = myproc();
// Must acquire p->lock in order to
// change p->state and then call sched.
// Once we hold p->lock, we can be
// guaranteed that we won't miss any wakeup
// (wakeup locks p->lock),
// so it's okay to release lk.
//获取当前任务锁
//释放tickslock锁
if(lk != &p->lock){ //DOC: sleeplock0
acquire(&p->lock); //DOC: sleeplock1
release(lk);
}
// Go to sleep.
//任务状态设置为SLEEPING状态,并且当前线程睡眠在ticks计数器上
p->chan = chan;
p->state = SLEEPING;
//执行任务调度
sched();
// Tidy up.
p->chan = 0;
// Reacquire original lock.
//释放任务锁,获取tickslock锁
if(lk != &p->lock){
release(&p->lock);
acquire(lk);
}
}
获取当前任务的lock,是为了改变当前任务状态时的并发安全性
- kernel/trap.c中的clockintr函数会在发生时钟中断时被调用
void
clockintr()
{
//获取tickslock
acquire(&tickslock);
//记录当前时钟中断发生次数
ticks++;
//唤醒所有睡眠在ticks计数器上的任务
wakeup(&ticks);
//释放锁
release(&tickslock);
}
- kernel/proc.c中的wakeup函数代码如下:
// Wake up all processes sleeping on chan.
// Must be called without any p->lock.
void
wakeup(void *chan)
{
//遍历任务列表
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
//唤醒所有睡眠在ticks计数器上的任务
if(p->state == SLEEPING && p->chan == chan) {
//设置对应任务状态为RUNNING,该任务会在之后的任务调度过程中被调度执行
//然后执行时,检查自身sleep time是否到期,如果没有到期,则继续sleep
//然后再次唤醒,再次检查,循环往复...
p->state = RUNNABLE;
}
release(&p->lock);
}
}
xv6中的sleep函数本质就是软件定时器的实现,但是其思路并不是在每次时钟中断发生时,唤醒所有到期的定时任务,而是直接唤醒所有睡眠的任务,让其自身去检查是否睡够了,如果没睡够,那么就继续接着睡。
这种实现方式的坏处就是定时任务的定时属性不够精准,而且唤醒了还未睡够的任务,造成资源浪费。
- 在kernel/start.c的timerinit定时器中断初始化方法中我们可以看到,时钟中断的触发间隔大约为1毫秒,也就是说ticks大约是每毫秒累加一次,即: 我们sleep函数的参数单位也是毫秒
Lab代码实现
经过上面的分析后,我们已经知道了sleep函数背后的原理,下面开始编写本lab的代码:
#include "kernel/types.h"
#include "user/user.h"
int main(int argc, char const *argv[])
{
//参数错误--第一个参数默认为当前程序名
if (argc != 2) {
fprintf(2, "usage: sleep <time>\n");
exit(1);
}
printf("sleep time=%s\n",argv[1]);
int ticks = atoi(argv[1]);
sleep(ticks);
printf("(nothing happens for a little while)\n");
exit(0);
}
执行测试:
- make clean
- make qemu
pingpong(难度:Easy)
任务:
- 编写一个使用UNIX系统调用的程序来在两个进程之间“
ping-pong
”一个字节,请使用两个管道,每个方向一个。 - 父进程应该向子进程发送一个字节;
- 子进程应该打印“
<pid>: received ping
”,其中<pid>
是进程ID,并在管道中写入字节发送给父进程,然后退出; - 父级应该读取从子进程而来的字节,打印“
<pid>: received pong
”,然后退出。 - 您的解决方案应该在文件
user/pingpong.c
中。
提示:
- 使用pipe来创造管道
- 使用fork创建子进程
- 使用read从管道中读取数据,并且使用write向管道中写入数据
- 使用getpid获取调用进程的pid
- 将程序加入到Makefile的UPROGS
- xv6上的用户程序有一组有限的可用库函数。您可以在user/user.h中看到可调用的程序列表;源代码(系统调用除外)位于user/ulib.c、user/printf.c和user/umalloc.c中。
运行程序应得到下面的输出:
$ make qemu
...
init: starting sh
$ pingpong
4: received ping
3: received pong
$
如果您的程序在两个进程之间交换一个字节并产生如上所示的输出,那么您的解决方案是正确的。
Lab代码实习
使用两个管道进行父子进程通信,需要注意的是如果管道的写端没有close,那么管道中数据为空时对管道的读取将会阻塞。因此对于不需要的管道描述符,要尽可能早的关闭。
#include "kernel/types.h"
#include "user/user.h"
#define RD 0 //pipe的read端
#define WR 1 //pipe的write端
int main(int argc, char const *argv[]) {
char buf = 'P'; //用于传送的字节
int fd_c2p[2]; //子进程->父进程
int fd_p2c[2]; //父进程->子进程
pipe(fd_c2p);
pipe(fd_p2c);
int pid = fork();
int exit_status = 0;
if (pid < 0) {
fprintf(2, "fork() error!\n");
close(fd_c2p[RD]);
close(fd_c2p[WR]);
close(fd_p2c[RD]);
close(fd_p2c[WR]);
exit(1);
} else if (pid == 0) { //子进程
close(fd_p2c[WR]);
close(fd_c2p[RD]);
if (read(fd_p2c[RD], &buf, sizeof(char)) != sizeof(char)) {
fprintf(2, "child read() error!\n");
exit_status = 1; //标记出错
} else {
fprintf(1, "%d: received ping\n", getpid());
}
if (write(fd_c2p[WR], &buf, sizeof(char)) != sizeof(char)) {
fprintf(2, "child write() error!\n");
exit_status = 1;
}
close(fd_p2c[RD]);
close(fd_c2p[WR]);
exit(exit_status);
} else { //父进程
close(fd_p2c[RD]);
close(fd_c2p[WR]);
if (write(fd_p2c[WR], &buf, sizeof(char)) != sizeof(char)) {
fprintf(2, "parent write() error!\n");
exit_status = 1;
}
if (read(fd_c2p[RD], &buf, sizeof(char)) != sizeof(char)) {
fprintf(2, "parent read() error!\n");
exit_status = 1; //标记出错
} else {
fprintf(1, "%d: received pong\n", getpid());
}
close(fd_p2c[WR]);
close(fd_c2p[RD]);
exit(exit_status);
}
}
- 测试
小结
实验一后续还有一些实验内容,留作后续慢慢补充