MIT 6.S081 Lab 1: Xv6 and Unix utilities
在这个实验中要实现几个用户级别的应用程序,其对应的系统调用在kernel中都已经被实现好了。
sleep
本实验要为 xv6 实现 UNIX 程序 sleep; 您的睡眠应暂停用户指定的滴答数。 滴答是 xv6 内核定义的时间概念,即来自定时器芯片的两次中断之间的时间。
我们检查参数,如果出现不是数字的参数就exit(-1),否则进行sleep。代码如下所示:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(int argc,char *argv[]){
while(argv[1][i]!=''){
if(argv[1][i]>'9'||argv[1][i]<'0'){
write(1, "error\n", 6);
exit(-1);
}
i++;
}
int times=atoi(argv[1]);
sleep(times);
exit(0);
}
值得注意的是,程序中我们使用了一些系统调用函数,如sleep函数,write函数。我们可以在user/user.h中一窥这些函数的原型:
我们来分析一下write函数,我们可以看到write函数的声明为int write(int,const void*,int);其中,参数中第一个int为文件描述符fd,参数中第二个const void*为内存地址,第三个int为写入的字节数量,意思就是说:将参数buf所指的内存写入count个字节到参数fd所指的文件,其中,fd为文件描述符。大家可以看到上述代码中的write函数的调用:write(1,"error\n",6),其中fd=1代表标准输出stdout,也就是会打印到显示器。
我们来看看write在内核中的实现sys_write:
uint64
sys_write(void)
{
struct file *f;//存储文件描述符
int n;//存储写入的字节数量
uint64 p;//存储内存地址
if(argfd(0, 0, &f) < 0 || argint(2, &n) < 0 || argaddr(1, &p) < 0)//分别写入
return -1;
return filewrite(f, p, n);
}在进入内核的时候,系统调用函数会将参数保存在寄存器中,然后调用argfd等函数将参数取出,保存在f,p,n中,调用filewrite函数。我们注意到struct file结构,其结构如下所示:
struct file {
enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
int ref; // reference count
char readable;
char writable;
struct pipe *pipe; // FD_PIPE
struct inode *ip; // FD_INODE and FD_DEVICE
uint off; // FD_INODE
short major; // FD_DEVICE
}filewrite是xv6操作系统中用于向文件写入数据的系统级函数。它在内核层面处理所有文件写入操作。函数参数 filewrite(struct file **f*, uint64 *addr*, int *n*)
f: 文件结构体指针,包含文件的所有元数据addr: 用户空间的虚拟地址,指向要写入的数据n: 要写入的字节数
根据文件类型执行不同的写入操作:
- 管道文件 (FD_PIPE):
if(f->type == FD_PIPE){
ret = pipewrite(f->pipe, addr, n);
}调用pipewrite函数处理管道写入。
- 设备文件 (FD_DEVICE)
else if(f->type == FD_DEVICE){
if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
return -1;
ret = devsw[f->major].write(1, addr, n);
}检查设备号是否有效,然后调用相应设备的写函数。
普通文件 (FD_INODE):这部分最复杂,需要特殊处理。
分块写入数据:
int i = 0;
while(i < n){
int n1 = n - i;
if(n1 > max)
n1 = max;
// ...写入逻辑...
i += r;
}每次写入块时都使用事务保证原子性:
begin_op(); // 开始事务
ilock(f->ip); // 锁定inode
if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
f->off += r; // 更新文件偏移量
iunlock(f->ip); // 解锁inode
end_op(); // 结束事务说到这里了,我们提一下buffer IO,我感觉xv6是没有实现buffer IO的,源码里面没有找到相关的说明和代码,但是在linux里面是实现了的。buffer IO是为了提高读写效率和保护磁盘,比如我们通过read函数将fd对应的文件拷贝count个字节到buf对应的内存,这个时候如果是buffer io机制,那么我们就会先将count个字节拷贝到page cache中,然后再拷贝到buf对应的用户空间中。write操作类似。
上次有位面试官问了我一个问题:什么时候将脏页刷回磁盘?
我查了一下,有的说是进程退出的时候刷回去,有的说是定时刷回去。在CMU15445细说。
我们将在后面的实验中继续学习syscall。
pingpong
编写一个程序,使用 UNIX 系统调用在两个进程之间通过一对管道“乒乓”一个字节,每个管道一个。 父母应该向孩子发送一个字节; 子进程应该打印“
一些提示:
- 使用管道创建管道。
- 使用 fork 创建一个孩子。
- 使用 read 从管道读取,并使用 write 写入管道。
- 使用 getpid 查找调用进程的进程 ID。
- 将程序添加到 Makefile 中的 UPROGS。
- xv6 上的用户程序有一组有限的库函数可供它们使用。 可以在 user/user.h 中看到列表; 源(系统调用除外)位于 user/ulib.c、user/printf.c 和 user/umalloc.c。
代码如下:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int main(){
int p_filedes[2],s_filedes[2];
pipe(p_filedes);
pipe(s_filedes);
char buf[4];
if(fork()==0){
read(p_filedes[0],buf,4);
printf("%d: received %s\n",getpid(),buf);
write(s_filedes[1],"pong",4);
}else{
write(p_filedes[1],"ping",4);
read(s_filedes[0],buf,4);
printf("%d: received %s\n",getpid(),buf);
}
exit(0);
}在上述代码中,我们使用了两个管道,p_filedes和s_filedes,来传递父进程和子进程之间的信息。
借此机会,我们来分析一下read函数,read函数原型为read(int,void *,int),其意义为将文件描述符fd所指向的文件读取count个数据到buf中。如read(0,buf,10)就是将标准输入读取10个字节到buf中。其底层实现为sys_read,
这让我想到了HUST大三学生在2021年做的lab1,通过程序演示多进程并发执行和进程软中断、管道通信。实验具体描述如下:
父进程先建立一个管道,然后创建两个进程:子进程1和子进程2;
父进程每隔1秒向管道发送消息(消息数量有上限) :
I send you x times. (x的初值为1,每次发送后对x做加1操作)
子进程1、2从管道接收消息,并显示在屏幕上。
- 父进程能捕获软中断信号SIGINT(按键盘的Ctrl+C键),捕获到该信号后,父进程分别向两个子进程发出软中断信号SIGUSR1。
- 子进程能捕获父进程发出的SIGUSR1信号,捕获到该信号后,分别输出下列信息后终止:
Child Process l is Killed by Parent!
Child Process 2 is Killed by Parent! - 父进程等待两个子进程终止后,释放管道并输出如下信息后终止:
Parent Process is Killed!
这个实验可以通过如下框架进行设计:
main( )
{
创建无名管道;
设置信号SIGINT处理;
创建子进程1、2;
定时发送数据;
等待子进程1、2退出;
关闭管道;
打印信息、退出;
}
父进程SIGINT信号处理
{
发SIGUSR1给子进程1;
发SIGUSR2给子进程2;
等待子进程1、2退出;
关闭管道;
打印信息、退出;
}
子进程1/2
{
设置信号SIGINT处理;
设置SIGUSR1或2处理;
while(1) {
从管道接收数据;
显示数据;
计数器++;
}
关闭管道;
打印信息、退出;
}
SIGUSR1/2信号处理
{
关闭管道;
打印信息;
退出;
}具体代码如下:
#include <unistd.h>
#include <stdlib.h>
#include<string.h>
#include<stdio.h>
#include<time.h>
#include <sys/wait.h>
#include <sys/types.h>
int pid_1,pid_2;
int filedes_1[2],filedes_2[2];
void fun(int sig)
{
kill(pid_1,SIGUSR1);
kill(pid_2,SIGUSR1);
waitpid(pid_1,NULL,0);
waitpid(pid_2,NULL,0);
close(filedes_1[0]);
close(filedes_1[1]);
close(filedes_2[0]);
close(filedes_2[1]);
printf("Parent Process is Killed!\n");
exit(0);
}
void fun1(int sig)
{
printf("\nChild1 process1 is killed by parent!\n");
close(filedes_1[0]);
close(filedes_1[1]);
exit(0);
}
void fun2(int sig)
{
printf("\nChild2 process2 is killed by parent!\n");
close(filedes_2[0]);
close(filedes_2[1]);
exit(0);
}
int main(){
pipe(filedes_1);
pipe(filedes_2);
char s[80];
int x=0;
pid_1=fork();
if(pid_1>0){
pid_2=fork();
if(pid_2>0){
signal(SIGINT,fun);
while(1){
x++;
char buf[80];
sprintf(buf, "I send you %d times", x);
write(filedes_1[1],buf,sizeof(buf));
write(filedes_2[1],buf,sizeof(buf));
sleep(1);
}
return 0;
}
else{
signal(SIGINT,SIG_IGN);
signal(SIGUSR1,fun2);
while(1){
read(filedes_2[0],s,sizeof(s));
printf("%s c2\n",s);
sleep(1);
}
}
}
else{
signal(SIGINT,SIG_IGN);
signal(SIGUSR1,fun1);
while(1){
read(filedes_1[0],s,sizeof(s));
printf("%s c1\n",s);
sleep(1);
}
}
}
xargs
xarg是给命令传递参数的一个过滤器,也是组合多个命令的一个工具。xarg可以将管道或标准输入数据转换成命令行参数,也能够从文件的输出中读取数据。
在实验中我们要完成的xargs程序与linux命令类似。
代码如下:
#include "kernel/types.h"
#include "kernel/param.h"
#include "user/user.h"
int main(int argc, char *argv[]) {
char line[256], *p[MAXARG], ch;
int lines = 0, linen, ps = 0, pn, i, j;
for (i = 0; i < argc - 1; i++) {
p[ps++] = line + lines;
for (j = 0; j < strlen(argv[i + 1]); j++)
line[lines++] = argv[i + 1][j];
line[lines++] = '\0';
}
linen = lines; pn = ps; p[pn++] = line + linen;
while (read(0, &ch, 1) > 0) {
if (ch == '\n') {
line[linen++] = '\0'; p[pn++] = 0;
if (fork() == 0) exec(argv[1], p);
else {
wait(0); linen = lines; pn = ps; p[pn++] = line + linen;
}
} else if (ch == ' ') {
line[linen++] = '\0'; p[pn++] = line + linen;
} else line[linen++] = ch;
}
exit(0);
}