XV6 Lab Lab1: Xv6 and Unix utilities xv6的安装以及启动网络上有很详细全面的教程,我就不在此赘述
1.sleep(难度:Easy) 实现xv6的UNIX程序 sleep
:您的 sleep
应该暂停到用户指定的计时数。一个滴答(tick)是由xv6内核定义的时间概念,即来自定时器芯片的两个中断之间的时间。
实验中需要将编写好的c语言sleep
程序添加到 Makefile 中的UPROGS
中;完成之后,make qemu
将编译您的程序,并且您可以从xv6的shell运行它。
在c语言中 argc参数代表shell接收到的参数个数,argv就是接收到的具体参数
注意c语言中的头文件引入很可能有互相的依赖,所以引入的顺序是一定的,使用clang需要关闭自动按字母排序的选项
记得make qemu
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" int main (int argc,char *argv[]) { if (argc!=2 ){ fprintf (2 ,"Usage: sleep [time]/n" ); exit (1 ); } int time=atoi(argv[1 ]); sleep(time); exit (0 ); }
2.pingpong(难度:Easy) 编写一个使用UNIX系统调用的程序来在两个进程之间“ping-pong”一个字节,请使用两个管道,每个方向一个。父进程应该向子进程发送一个字节;子进程应该打印“<pid>: received ping
”,其中<pid>
是进程ID,并在管道中写入字节发送给父进程,然后退出;父级应该从读取从子进程而来的字节,打印“<pid>: received pong
”,然后退出。
管道的概念: 管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:
其本质是一个文件(实为内核缓冲区)
由两个文件描述符引用,一个表示读端,一个表示写端。
规定数据从管道的写端流入管道,从读端流出。
管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
管道的局限性:
① 数据自己读不能自己写。
② 数据一旦被读走,便不在管道中存在,不可反复读取。
③ 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
④ 只能在有公共祖先的进程间使用管道。
1 2 3 4 5 6 7 8 9 #include <unistd.h> int pipe (int fd[2 ]) ;int getpid () 用来取得目前进程的进程识别码
linux下创建管道可以通过函数pipe来完成。该函数如果调用成功,数组中将包含两个新的文件描述符。
管道两端可分别用描述符fd[0] 以及fd[1]来描述。需要注意的是,管道两端的任务是固定的,一端只能用于读,由描述符fd[0]表示,称其为管道读端;
另一端只能用于写,由描述符fd[1]来表示,称其为管道写端。如果试图从管道写端读数据,或者向管道读端写数据都将导致出错。
管道是一种文件,因此对文件操作的I/O函数都可以用于管道,如read,write等。
管道一旦创建成功,就可以作为一般的文件来使用,对一般文件进行操作的I/O函数也适用于管道文件。
管道的一般用法是,进程在使用fork函数创建子进程前先创建一个管道,该管道用于在父子进程间通信,然后创建子进程,之后父进程关闭管道的读端,子进程关闭管道的写端。父进程负责向管道写数据而子进程负责读数据。当然父进程可以关闭管道的写端而子进程关闭管道的读端。这样管道就可以用于父子进程间的通信,也可以用于兄弟进程间的通信。
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 #include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" int main (int argc, char *argv[]) { int p2c[2 ]; int c2p[2 ]; if (pipe(p2c) < 0 ) { printf ("pipe" ); exit (-1 ); } if (pipe(c2p) < 0 ) { printf ("pipe" ); exit (-1 ); } int pid = fork(); if (pid == 0 ) { char buf[10 ]; read(p2c[0 ], buf, 10 ); printf ("%d: received ping\n" , getpid()); write(c2p[1 ], "pong" , 5 ); } else if (pid > 0 ) { write(p2c[1 ], "ping" , 5 ); char buf[10 ]; read(c2p[0 ], buf, 10 ); printf ("%d: received pong\n" , getpid()); } close(p2c[0 ]); close(p2c[1 ]); close(c2p[0 ]); close(c2p[1 ]); exit (0 ); }
3.Primes(素数,难度:Moderate/Hard) 使用管道编写 prime sieve
(筛选素数)的并发版本
实验前置知识中文翻译
提示:当管道的write
端关闭时,read
返回零
“当管道的write端关闭时,read返回零”,这指的是当管道的写入端(write端)被关闭后,任何试图从管道的读取端(read端)读取数据的操作如果已经没有更多的数据可读,则会返回0。这里的0并不是表示读到了什么数据,而是表示文件结束(EOF,End Of File)状态。
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 #include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" void prime (int rd) { int n; read( rd, &n, 4 ); printf ("prime %d\n" , n); int created = 0 ; int fd[2 ]; int num; while (read(rd, &num, 4 ) == 4 ) { if (created == 0 ) { if (pipe(fd) < 0 ) { fprintf (2 , "create pipe failed" ); exit (-1 ); }; created = 1 ; int pid = fork(); if (pid == 0 ) { close(fd[1 ]); prime(fd[0 ]); return ; } else { close(fd[0 ]); } } if (num % n != 0 ) { if (write(fd[1 ], &num, 4 ) < 0 ) { fprintf (2 , "write wrong" ); exit (-1 ); }; } } close(rd); close(fd[1 ]); wait(0 ); return ; } int main (int argc, char *argv[]) { int fd[2 ]; if (pipe(fd) < 0 ) { fprintf (2 , "create pipe failed" ); exit (-1 ); }; int pid = fork(); if (pid < 0 ) { fprintf (2 , "fork failed" ); exit (-1 ); } if (pid != 0 ) { close(fd[0 ]); for (int i = 2 ; i <= 35 ; i++) { if (write(fd[1 ], &i, 4 ) < 0 ) { fprintf (2 , "write wrong" ); exit (-1 ); }; } close(fd[1 ]); wait(0 ); } else { close(fd[1 ]); prime(fd[0 ]); close(fd[0 ]); } exit (0 ); }
4.find(难度:Moderate)
首先我们需要对ls函数源码进行阅读和学习
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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 #include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" #include "kernel/fs.h" char *fmtname (char *path) { static char buf[DIRSIZ+1 ]; char *p; for (p=path+strlen (path); p >= path && *p != '/' ; p--) ; p++; if (strlen (p) >= DIRSIZ) return p; memmove (buf, p, strlen (p)); memset (buf+strlen (p), ' ' , DIRSIZ-strlen (p)); return buf; } void ls (char *path) { char buf[512 ], *p; int fd; struct dirent de; struct stat st; if ((fd = open (path, 0 )) < 0 ){ fprintf (2 , "ls: cannot open %s\n" , path); return ; } if (fstat (fd, &st) < 0 ){ fprintf (2 , "ls: cannot stat %s\n" , path); close (fd); return ; } switch (st.type){ case T_FILE: printf ("%s %d %d %l\n" , fmtname (path), st.type, st.ino, st.size); break ; case T_DIR: if (strlen (path) + 1 (->"/" ) + DIRSIZ + 1 (->"\0" ) > sizeof buf){ printf ("ls: path too long\n" ); break ; } strcpy (buf, path); p = buf+strlen (buf); *p++ = '/' ; while (read (fd, &de, sizeof (de)) == sizeof (de)){ if (de.inum == 0 ) continue ; memmove (p, de.name, DIRSIZ); p[DIRSIZ] = 0 ; if (stat (buf, &st) < 0 ){ printf ("ls: cannot stat %s\n" , buf); continue ; } printf ("%s %d %d %d\n" , fmtname (buf), st.type, st.ino, st.size); } break ; } close (fd); } int main (int argc, char *argv[]) { int i; if (argc < 2 ){ ls ("." ); exit (0 ); } for (i=1 ; i<argc; i++) ls (argv[i]); exit (0 ); }
struct dinode
定义描述了存储在磁盘上的inode结构。inode(节点索引)是文件系统中的一个重要概念,用于存储文件的元数据。下面是对这个结构的详细解释:
1 2 3 4 5 6 7 8 struct dinode { short type; short major; short minor; short nlink; uint size; uint addrs[NDIRECT+1 ]; };
结构成员解释
short type
:
short major
和 short minor
:
类型:short
含义:设备的主要编号和次要编号。这些字段只在文件类型为设备文件(如块设备或字符设备)时有效。主要编号和次要编号一起唯一地标识了一个硬件设备。
short nlink
:
类型:short
含义:指向该inode的硬链接数量。如果一个文件有多个名字(即在不同的目录中有多个指向同一inode的链接),则nlink
字段的值会相应增加。
uint size
:
类型:uint
含义:文件的大小(以字节为单位)。这对于普通文件、符号链接等有意义,而对于目录或其他特殊文件,这个字段可能有不同的意义。
uint addrs[NDIRECT+1]
:
类型:uint
数组
含义:数据块地址数组。这个数组包含了指向文件数据的实际磁盘块地址。NDIRECT
通常定义为一个具体的数值,表示直接块的数量。数组中最后一个元素(addrs[NDIRECT]
)通常用于指向间接块(单级、多级间接块等),以支持更大的文件。
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 #include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" #include "kernel/fs.h" void search (char *path, char *target) ;char *fmtname (char *path) { static char buf[DIRSIZ + 1 ]; char *p; for (p = path + strlen (path); p >= path && *p != '/' ; p--) ; p++; if (strlen (p) >= DIRSIZ) return p; memmove(buf, p, strlen (p)); memset (buf + strlen (p), 0 , DIRSIZ - strlen (p)); return buf; } int main (int argc, char *argv[]) { if (argc != 3 ) { fprintf (2 , "Usage:find <pathname> <filename>\n" ); exit (1 ); } search(argv[1 ], argv[2 ]); exit (0 ); } void search (char *path, char *target) { char *p; int fd; struct dirent de ; struct stat st ; if ((fd = open(path, 0 )) < 0 ) { fprintf (2 , "open err\n" ); exit (1 ); } if (fstat(fd, &st) < 0 ) { fprintf (2 , "fstat err\n" ); exit (1 ); } switch (st.type) { case T_FILE: if (!strcmp (target, fmtname(path))) { printf ("%s\n" , path); } break ; case T_DIR: p = path + strlen (path); *p++ = '/' ; while (read(fd, &de, sizeof (de)) == sizeof (de)) { if (de.inum == 0 ) continue ; memmove(p, de.name, DIRSIZ); p[DIRSIZ] = 0 ; if (strcmp (p, "." ) && strcmp (p, ".." )) search(path, target); memset (p, 0 , DIRSIZ); } } close(fd); }
理解了ls的代码就会发现find的代码非常容易编写,其实就是ls加上了一点点比较就可以实现
5.xargs(难度:Moderate) 既然我们要仿照实现一个xargs,我们就先看看linux中的args是什么样的
xargs 命令教程
Linux xargs 命令
命令行参数和标准输入的区别 一般来说,向程序传递数据可以采用参数传递的方式,在调用的同时将数据一并传递;或者使用标准输入,在程序启动后通过控制台键入等方式输入数据。下面我们通过一个简单的例子说明二者的区别。我们实现一个简单的 echo
命令,分别采用参数传递和标准输入两种传递数据的方式,接受一个字符串并打印。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdin.h> int main (int argc, char * argv[]) { printf ("%s\n" , argv[1 ]); return 0 ; } #include <stdio.h> int main (int argc, char * argv[]) { char str[128 ]; scanf ("%s" , str); printf ("%s\n" , str); return 0 ; }
要注意这两个程序有不同的启动方式。
1 2 3 4 5 ~ % ./echo1 tauyoung # 启动的同时一并传递参数 tauyoung # 打印传递的参数值 ~ % ./echo2 # 启动时不接受任何参数 tauyoung # 在控制台通过键盘输入 tauyoung # 打印输入的数据
在大部分情况下我们可以采用任意一种方式传递数据,只要程序能够正常运行。但是,在配合其他程序或者脚本使用时,也应该采用相应的方式启动。例如,我们希望通过我们实现的 echo
输出当前用户,下面是这两个程序的正确启动方式:
1 2 3 4 ~ % ./echo1 $(whoami) # 将用户名作为参数传递给 echo1 tauyoung ~ % whoami | ./echo2 # 通过管道将用户名传递给 echo2 的标准输入 tauyoung
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 #include "kernel/types.h" #include "kernel/stat.h" #include "user/user.h" #include "kernel/param.h" int readline (char *new_argv[32 ], int curr_argc) { char buf[1024 ]; int n = 0 ; while (read(0 , buf + n, 1 ) == 1 ) { if (n == 1023 ) { fprintf (2 , "argument is too long\n" ); exit (-1 ); } if (buf[n] == '\n' ) { break ; } n++; } buf[n] = 0 ; if (n == 0 ) return 0 ; int offset = 0 ; while (offset < n) { new_argv[curr_argc++] = buf + offset; while (buf[offset] != ' ' && offset < n) { offset++; } while (buf[offset] == ' ' && offset < n) { buf[offset++] = 0 ; } } return curr_argc; } int main (int argc, char const *argv[]) { if (argc < 2 ) { printf ("Usage: xargs [command]\n" ); exit (-1 ); } char *command = malloc (strlen (argv[1 ]) + 1 ); char *new_argv[MAXARG]; strcpy (command, argv[1 ]); for (int i = 1 ; i < argc; ++i) { new_argv[i - 1 ] = malloc (strlen (argv[i]) + 1 ); strcpy (new_argv[i - 1 ], argv[i]); } int curr_argc; while ((curr_argc = readline(new_argv, argc - 1 )) != 0 ) { new_argv[curr_argc] = 0 ; int pid = fork(); if (pid < 0 ) { fprintf (2 , "fork failed\n" ); exit (-1 ); } if (pid == 0 ) { exec(command, new_argv); fprintf (2 , "exec failed\n" ); exit (-1 ); } wait(0 ); } for (int i = 0 ; i < curr_argc; ++i) { free (new_argv[i]); } free (command); exit (0 ); }