MIT_6.s081_Lab1:Xv6 and Unix utilities

在这个实验中要实现几个用户级别的应用程序,其对应的系统调用在kernel中都已经被实现好了。

sleep

本实验要为 xv6 实现 UNIX 程序 sleep; 您的睡眠应暂停用户指定的滴答数。 滴答是 xv6 内核定义的时间概念,即来自定时器芯片的两次中断之间的时间。

我们检查参数,如果出现不是数字的参数就exit(-1),否则进行sleep。代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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:

1
2
3
4
5
6
7
8
9
10
11
12
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 *f结构,其结构如下所示:

1
2
3
4
5
6
7
8
9
10
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
}

说到这里了,我们提一下buffer IO,我感觉xv6是没有实现buffer IO的,源码里面没有找到相关的说明和代码,但是在linux里面是实现了的。buffer IO是为了提高读写效率和保护磁盘,比如我们通过read函数将fd对应的文件拷贝count个字节到buf对应的内存,这个时候如果是buffer io机制,那么我们就会先将count个字节拷贝到page cache中,然后再拷贝到buf对应的用户空间中。write操作类似。

上次面试官问了我一个问题:什么时候将脏页刷回磁盘?

我查了一下,有的说是进程退出的时候刷回去,有的说是定时刷回去。在CMU15445细说。


我们将在后面的实验中继续学习syscall。

pingpong

编写一个程序,使用 UNIX 系统调用在两个进程之间通过一对管道“乒乓”一个字节,每个管道一个。 父母应该向孩子发送一个字节; 子进程应该打印“: received ping”,其中 是它的进程 ID,将管道上的字节写入父进程,然后退出; 父母应该从孩子那里读取字节,打印“: received pong”,然后退出。

一些提示:

  • 使用管道创建管道。
  • 使用 fork 创建一个孩子。
  • 使用 read 从管道读取,并使用 write 写入管道。
  • 使用 getpid 查找调用进程的进程 ID。
  • 将程序添加到 Makefile 中的 UPROGS。
  • xv6 上的用户程序有一组有限的库函数可供它们使用。 可以在 user/user.h 中看到列表; 源(系统调用除外)位于 user/ulib.c、user/printf.c 和 user/umalloc.c。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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!

这个实验可以通过如下框架进行设计:

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
main( )
{
创建无名管道;
设置信号SIGINT处理;
创建子进程12;
定时发送数据;
等待子进程12退出;
关闭管道;
打印信息、退出;
}
父进程SIGINT信号处理
{
发SIGUSR1给子进程1;
发SIGUSR2给子进程2;
等待子进程12退出;
关闭管道;
打印信息、退出;
}
子进程1/2
{
设置信号SIGINT处理;
设置SIGUSR1或2处理;
while(1) {
从管道接收数据;
显示数据;
计数器++;
}
关闭管道;
打印信息、退出;
}
SIGUSR1/2信号处理
{
关闭管道;
打印信息;
退出;
}

具体代码如下:

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
#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命令类似。

代码如下:

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