本文为 MIT 6.S081 2020 操作系统 实验一解析。
MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列
任务:
一个滴答(tick)是由xv6内核定义的时间概念,即来自定时器芯片的两个中断之间的时间。您的解决方案应该在文件user/sleep.c中
Tips:
/user/echo.c, /user/grep.c, /user/rm.c
)查看如何获取传递给程序的命令行参数详见/user/ulib.c
)kernel/sysproc.c
以获取实现sleep系统调用的xv6内核代码(查找sys_sleep
),user/user.h
提供了sleep的声明以便其他程序调用,用汇编程序编写的user/usys.S
可以帮助sleep从用户区跳转到内核区。运行效果:
$ 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
#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);
}
int atoi(const char *s){
int n=0;
while('0' <= *s && *s <= '9')
//每次处理一个字符,n每次乘10进一位,然后*s-'0'计算出当前字符代表数字几
n = n*10 + *s++ - '0';
return n;
}
int sleep(int);
.global sleep
sleep:
li a7, SYS_sleep
ecall
ret
// 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
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;
}
// 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,是为了改变当前任务状态时的并发安全性
void
clockintr()
{
//获取tickslock
acquire(&tickslock);
//记录当前时钟中断发生次数
ticks++;
//唤醒所有睡眠在ticks计数器上的任务
wakeup(&ticks);
//释放锁
release(&tickslock);
}
// 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函数本质就是软件定时器的实现,但是其思路并不是在每次时钟中断发生时,唤醒所有到期的定时任务,而是直接唤醒所有睡眠的任务,让其自身去检查是否睡够了,如果没睡够,那么就继续接着睡。
这种实现方式的坏处就是定时任务的定时属性不够精准,而且唤醒了还未睡够的任务,造成资源浪费。
经过上面的分析后,我们已经知道了sleep函数背后的原理,下面开始编写本lab的代码:
#include "kernel/types.h"
#include "user/user.h"
int main(int argc, char const *argv[])
{
//参数错误--第一个参数默认为当前程序名
if (argc != 2) {
fprintf(2, "usage: sleep );
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);
}
任务:
ping-pong
”一个字节,请使用两个管道,每个方向一个。: received ping
”,其中
是进程ID,并在管道中写入字节发送给父进程,然后退出;: received pong
”,然后退出。user/pingpong.c
中。提示:
运行程序应得到下面的输出:
$ make qemu
...
init: starting sh
$ pingpong
4: received ping
3: received pong
$
如果您的程序在两个进程之间交换一个字节并产生如上所示的输出,那么您的解决方案是正确的。
使用两个管道进行父子进程通信,需要注意的是如果管道的写端没有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);
}
}
实验一后续还有一些实验内容,留作后续慢慢补充