在这个实验中要实现几个用户级别的应用程序,其对应的系统调用在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; char readable; char writable; struct pipe *pipe; struct inode *ip; uint off; short major; }
|
说到这里了,我们提一下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处理; 创建子进程1、2; 定时发送数据; 等待子进程1、2退出; 关闭管道; 打印信息、退出; } 父进程SIGINT信号处理 { 发SIGUSR1给子进程1; 发SIGUSR2给子进程2; 等待子进程1、2退出; 关闭管道; 打印信息、退出; } 子进程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); }
|