阅读下面两个源代码,理解xv6及其系统调用
syscall.h
是对xv6里常见的21个系统调用的宏定义。定义指向实现函数的系统调用向量的位置。
在syscall.c中外部定义(extern)一个链接内核和shell的函数。
// 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
sysproc.c
与进程创建、退出等相关的系统调用函数的实现。
#include "types.h" // typedef uint, ushort, uchar; uint8, uint16, uint32, uint64; pde_t;
#include "riscv.h"
#include "defs.h" // struct & functions
#include "date.h" // struct rtcdate{uint second, minute, hour, day, month, year; }
#include "param.h" // define NPROC NCPU NOFILE NFILE NINODE NDEV ROOTDEV MAXARG MAXOPBLOCKS LOGSIZE NBUF FSSIZE MAXPATH
#include "memlayout.h" // define UART0 UART0_IRQ VIRTIO0 VIRTIO_IRQ PLIC PLIC_PRIORITY PLIC_PENDING PLIC_MENABLE PLIC_SENABLE PLIC_MPRIORITY PLIC_SPRIORITY PLIC_MCLAIM PLIC_SCLAIM KERNBASE PHYSTOP TRAMPOLINE KSTACK TRAPFRAME
#include "spinlock.h" // struct spinlock{uint locked; char *name; struct cpu *cpu;}
#include "proc.h" // struct context{uint64 ra, sp, s0~s11;} cpu{proc *proc; context *context; int noff, intena;} trapframe{uint64 kernel_satp, kernel_sp, kernel_trap, epc, kernel_hartid, ra, sp, gp, tp, t0~t2, s0~s1, a0~a7, s2~s11, t3~t6;} enum procstate {UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE} proc{spinlock lock; procstate state; void *chan; int killed, xstate, pid; uint64 kstack, sz; page_table_t pagetable; trapframe *trapframe; context context; file *ofile[NOFILE]; inode *cwd; char name[16]}
uint64
sys_exit(void)
{
int n;
if(argint(0, &n) < 0)
return -1;
exit(n);
return 0; // not reached
}
uint64
sys_getpid(void)
{
return myproc()->pid;
}
uint64
sys_fork(void)
{
return fork();
}
uint64
sys_wait(void)
{
uint64 p;
if(argaddr(0, &p) < 0)
return -1;
return wait(p);
}
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
if(growproc(n) < 0)
return -1;
return addr;
}
uint64
sys_sleep(void)
{
int n;
uint ticks0;
if(argint(0, &n) < 0)
return -1;
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(myproc()->killed){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}
uint64
sys_kill(void)
{
int pid;
if(argint(0, &pid) < 0)
return -1;
return kill(pid);
}
// return how many clock tick interrupts have occurred
// since start.
uint64
sys_uptime(void)
{
uint xticks;
acquire(&tickslock);
xticks = ticks;
release(&tickslock);
return xticks;
}
sysproc.c中的系统调用函数都没有参数,而内部的kill, sleep函数却含有参数。这是因为在XV6中无法将参数从用户级函数传递到内核级函数,例如:XV6中用argint()函数来传递整数。
argint(); // 传递整数
argptr(); //传递指针
argstr(); // 传递字符串起始地址
安装环境:参考官方文档xv6-tools
克隆xv6仓库: git://g.csail.mit.edu/xv6-labs-2020
运行xv6:make qemu
退出qemu:Ctrl-a + x
自我评分:make grade
xv6:一个kernel,许多进程(指令、数据、堆栈)
内容:processes, memory, file descriptors, pipes, a file system.
shell:a user program. xv6 shell的实现在user/sh.c:1中可以找到。
Xv6 system calls
If not otherwise stated, these calls return 0 for no error, and -1 if there’s an error.
int fork();
int exit(int status);
int wait(int *status);
int kill(int pid);
int getpid();
int sleep(int n);
int exec(char *file, char *argv[]);
char *sbrk(int n);
int open(char *file, char *argv[]);
int write(int fd, char *buf, int n);
int read(int fd, char *buf, int n);
int close(int fd);
int dup(int fd);
int pipe(int p[]);
int chdir(char *dir);
int mkdir(char *dir);
int mknod(char *file, int, int);
int fstat(int fd, struct stat *st);
int stat(char *file, struct stat *st);
int link(char *file1, char *file2);
int unlink(char *file);
fork创建一个子进程,内容和父进程相同。父级fork返回子级的PID,子级fork返回0.
ELF格式。exec有两个参数:包含可执行参数的文件名和字符串参数数组。
shell结构:
main(user/sh.c:145): 用getcmd读一行输入;调用fork,创建shell进程的副本;父进程调用wait,子进程执行命令。
runcmd(user/sh.c:58):执行实际命令
exec(user/sh.c:78):执行指令。
shell在实现I/O重定向时利用了分离。
sbrk(n):提供更多的内存(malloc可能调用)。返回新内存的位置。
pid = wait ((int *) 0);
printf("child %d is done\n", pid);
} else if(pid == 0){
printf("child: exiting\n");
exit(0);
} else{
printf("fork error\n");
}
文件描述符:一个小的整数,表示进程可以读取或写入的内核管理对象。指向的对象成为文件。0-标准输入,1-标准输出,2-标准错误。它们约定了cat的简单实现。
read(fd, buf, n):从fd文件描述符,读取最多n个字节,拷贝进buf,返回读取的字节数。
read从当前文件偏移量中读取数据,然后按照读取的字节数前进偏移量,随后的进程返回第一次读取返回的字节之后的字节;没有更多字节可读取时,read返回0,代表文件结束。
write(fd, buf, n)从buf写入n个字节进到fd文件中,返回写入字节数。同read,每个write从上一个偏移量停止的地方开始。
close释放一个文件描述符,使它可以被将来的open、pipe或dup重用(见下文)。新分配的文件描述符总是当前进程中编号最小的未使用描述符。
fork复制父级的文件描述符表及其内存,以便子级从与父级完全相同的打开文件开始。系统调用exec替换调用进程的内存,但保留其文件表。此行为允许shell通过分叉、重新打开子级中选定的文件描述符,然后调用exec来运行新程序来实现I/O重定向。
open: file control(fcntl) (kernel/fcntl.h:1-5): O_RDONLY, O_WRONLY, O_RDWR, O_CREATE, O_TRUNC。只读、只写、读写、创建、删节文件长度至0。
QUES:为什么fork和exec是分开的?为了方便I/O重定位。
dup复制一个现有的文件描述符,返回一个引用相同底层I/O对象的新文件描述符。两个文件描述符共享一个偏移量,就像fork复制的文件描述符一样。
char buf[512];
int n;
for(;;){
n = read(0, buf, sizof buf);
if(n == 0) break;
if(n<0){
fprintf(2, "read error\n");
exit(1);
}
if(write(1,buf,n)!=n){
fprintf(2, "write error\n");
exit(1);
}
}
--
if(fork() == 0){
write(1,"hello ", 6);
exit(0);
} else {
wait(0);
write(1, "world\n", 6);
}
--
fd = dup(1);
write(1, "hello ", 6);
write(fd, "world\n", 6);
pipe:给进程提供一种通信方式:数据写入管道的一段,可以从另一端读取。
如果没有数据可用,管道上的读取将等待写入数据或所有引用写入端的文件描述符被关闭;在后一种情况下,read将返回0,就像数据文件的结尾已经到达一样。
管道 | 临时文件 |
---|---|
自动清理自己 | 因为文件重定向,必须小心删除/tmp/xyz |
传递任意长的数据流 | 需要磁盘上有足够的可用空间来存储所有数据 |
允许并行执行管道阶段 | 第一个程序在第二个程序启动前完成 |
阻塞读写在进程间通信更有效 | 非阻塞语义 |
int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p);
if(fork() == 0) {
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
exec("/bin/wc", argv);
} else {
close(p[0]);
write(p[1], "hello world\n", 12);
close(p[1]);
}
数据文件:未解释的字节数组
目录:对数据文件和其他目录的命名引用
树:从一个名为root的特殊目录(根目录)开始
两种方式打开文件:
chdir("/a"); chdir("b"); open("c",O_RDONLY);
open("/a/b/c", O_RDONLY);
mkdir(""):创建新目录directory
open("",O_CREATE):创建新数据文件data file
mknod("", major device numbers, minor device numbers):创建新的设备文件device file 主设备号和次设备号唯一地标识内核设备。打开设备文件时,内核将读写系统调用转到内核设备实现,而不是传递到文件系统。
fstat:从文件描述符引用的inode检索信息。stat.h(kernel/stat.h)
link:创建另一个拥有与现有文件相同的inode的文件系统名称。
unlink:移除一个名称。文件链接计数为0且没有文件描述符引用它时,释放文件的inode和保存其内容的磁盘空间
mkdir, ln, rm都是用户级的程序。
例外是cd,是内置在shell(user/sh.c: 160)中的。因为cd会更改shell本身的当前工作目录,如果是常规命令的话,那么shell将派生一个子进程,子进程运行cd,cd只能修改子进程的工作目录,而不改变父进程shell的工作目录。
chdir("/a");
chdir("b");
open("c", O_RDONLY);
||
open("/a/b/c", O_RDONLY);
--
mkdir("/dir");
fd = open("dir/file", O_CREATE|O_WRONLY);
close(fd);
mknod("/console",1 ,1);
--
link("a", "b");
unlink("a");
UNIX引发了“软件工具”,shell是第一种“脚本语言”。
POSIX标准(可移植操作系统接口)
Unix通过一组文件名和文件描述符接口统一访问多种类型的资源(文件、目录、设备)
实现UNIX程序sleep,暂停用户指定的tick数。
tick:xv6内核定义的时间概念,即定时器芯片的两次中断之间的时间。
//echo.c
#include "kernel/types.h" //定义类型
#include "kernel/stat.h" //定义变量别名
#include "user/user.h" //定义系统调用接口和一些C函数
int
main(int argc, char *argv[])
{
int i;
for(i = 1; i < argc; i++){
write(1, argv[i], strlen(argv[i]));
if(i + 1 < argc){
write(1, " ", 1);
} else {
write(1, "\n", 1);
}
}
exit(0);
}
uint64
sys_sleep(void)
{
int n;
uint ticks0;
if(argint(0, &n) < 0)
return -1;
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(myproc()->killed){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}
int sleep(int);
//98~102
.global sleep
sleep:
li a7, SYS_sleep
ecall
ret
//sleep.c
// Sleep function. Pause for a user-specified number of ticks.
#include "kernel/types.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
int i;
if(argc != 2){
fprintf(2, "Usage: sleep needs 1 parameter, gets %d parameter(s) instead.\n", argc-1);
exit(1);
}
i = atoi(argv[1]);
if(i <= 0)
exit(1);
sleep(i);
exit(0);
}
修改Makefile中的UP: 152
$U/_sleep\
# this is changed when write sleep.c
./grade-lab-util sleep
//or
make GRADEFLAGS=sleep grade
实现程序pingpong,使用UNIX系统在两个进程之间通过管道传递一个byte
父进程将一个字节发送给子进程;子进程打印< pid>: received ping, 其中pid是其进程ID;子进程将字节发送给父进程,然后退出;父进程从子进程读取字节,打印< pid>: received pong, 然后退出。
#include "kernel/types.h"
#include "user/user.h"
int
main(int argc, char *argv[])
{
int p1[2], p2[2];
char buf[] = {'A'}; // one byte
int n = sizeof(buf);
pipe(p1);
pipe(p2);
if(fork() == 0) {
close(p1[1]);
close(p2[0]);
if(read(p1[0],buf,n) != n){
printf("Error: child read from p1 with parent failed.");
exit(1);
}
printf("%d: received ping\n", getpid());
if(write(p2[1],buf,n) != n){
printf("Error: child write into p2 with parent failed.");
exit(1);
}
exit(0);
} else {
close(p1[0]);
close(p2[1]);
if(write(p1[1],buf,n) != n){
printf("Error: parent write into p1 with child failed.");
exit(1);
}
if(read(p2[0],buf,n) != n){
printf("Error: parent read from p2 with child failed.");
exit(1);
}
printf("%d: received pong\n",getpid());
wait(0); //wait for child exit;
exit(0);
}
}
$U/_pingpong\
# this is changed when write pingpong.c
//ulib.c
char *strcpy(char *s, const char *t);
int strcmp(const char *p, const char*q);
uint strlen(const char *s);
void *memset(void *dst, int c, uint n);
char *strchr(const char *s, char c);
char *gets(char *buf, int max);
int stat(const char *n, struct stat *st);
int atoi(const char *s);
void *memmove(void *vdst, const void *vsrc, int n);
int memcmp(const void *s1, const void *s2, uint n);
void *memcpy(void *dst, const void *src, uint n);
//printf.c
static void putc(int fd, char c);
static void printint(int fd, int xx, int base, int sgn);
static void printptr(int fd, uint64 x);
void vprintf(int fd, const char *fmt, va_list ap);
void fprintf(int fd, const char *fmt, ...);
void printf(const char *fmt, ...);
//umalloc.c
void free(void *ap);
static Header* morecore(uint nu);
void *malloc(uint nbytes);
pipe函数用来打开管道,要记得在进程中关闭用不到的管道端。
父进程推出前要记得wait子进程exit。
用pipes编写并发版本的主筛,用pipe和fork建立管道。参阅贝尔实验室和CSP线程中的图片了解如何实现。
第一个进程将2到35压入管道,为每个素数创建一个进程,通过管道从其左邻居读取,并在另一管道上向其右邻居写入。第一个进程可以在35处停止。
//伪代码描述
p = get a number from left neighbor
print p
loop:
n = get a number from left neighbor
if (p does not divid n)
send n to right neighbor
//Primes Function. Print prime numbers from 2~35
#include "kernel/types.h"
#include "user/user.h"
void primes(int p[2]){
int n, prime;
int p1[2];
close(p[1]);
if(read(p[0], &prime, 4) == 0){
close(p[0]);
exit(1);
} else {
printf("prime %d\n", prime);
pipe(p1);
if(fork() == 0) {
close(p[0]);
primes(p1);
} else {
close(p1[0]);
while(1){
if(read(p[0],&n, 4) == 0) break;
if(n % prime != 0) write(p1[1], &n, 4);
}
close(p[0]);
close(p1[1]);
wait(0);
}
}
exit(0);
}
int
main(int argc, char *argv[]){
if(argc != 1){
fprintf(2," Usage: error.\n");
exit(1);
}
int n;
int p[2];
pipe(p);
if(fork() == 0) {
primes(p);
} else {
close(p[0]);
for(n = 2 ; n <= 35 ; n ++ ){
write(p[1], &n, 4);
}
close(p[1]);
wait(0);
}
exit(0);
}
写一个简单find.c程序,在目录树里根据一个特定名称找到文件。
//here's ls.c
#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;
// Find first character after last slash.
for(p=path+strlen(path); p >= path && *p != '/'; p--)
;
p++;
// Return blank-padded name.
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 > 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);
}
find.c的代码结构和ls.c没有太大区别,但三去除了fmtname中对buf相关的操作。
// Find Function.
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
char*
fmtname(char *path)
{
char *p;
//Find first character after last slash.
for(p=path+strlen(path); p >= path && *p != '/'; p--)
;
p++;
return p;
}
void
find(char *path, char *filename)
{
char buf[512], *p;
int fd;
struct dirent de;
struct stat st;
if((fd = open(path, 0)) < 0){
fprintf(2, "find: cannot open %s\n", path);
return;
}//open the file
if(fstat(fd, &st) < 0){
fprintf(2, "find: cannot stat %s\n", path);
close(fd);
return;
}//obtain some information about the file's stat
switch(st.type){
//fd is a file
case T_FILE:
if(strcmp(fmtname(path), filename) == 0){
printf("%s\n", path);
}
break;
// fd is a directory
case T_DIR:
if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
printf("find: path too long\n");
break;
}
strcpy(buf, path);
p = buf+strlen(buf);
*p++ = '/';
// open the next file
while(read(fd, &de, sizeof(de)) == sizeof(de)){//read to dirent
if((de.inum == 0) || strcmp(de.name, ".") == 0 || strcmp(de.name, "..") == 0)
continue;
memmove(p, de.name, DIRSIZ);
p[DIRSIZ] = 0;
find(buf, filename);
}
break;
}
close(fd);
}
int
main(int argc, char *argv[]){
if(argc != 3){
fprintf(2, "Usage: find directory filename.\n");
exit(1);
}
find(argv[1], argv[2]);
exit(0);
}
编写xargs.c程序,从标准输入中读取行,并为每行运行一个命令,将该行作为命令的参数。
//Xargs Funtion.
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
#include "kernel/param.h"
int
main(int argc, char *argv[])
{
char buf[512];
char *full_argv[MAXARG];
int i;
int len;
if(argc < 2){
fprintf(2, "Usage: xargs command\n");
exit(1);
}
if(argc + 1 > MAXARG){
fprintf(2, "Usage: too many args\n");
exit(1);
}
for(i=1 ; i < argc ; i++){
full_argv[i-1] = argv[i];
}
full_argv[argc] = 0;
while (1) {
i = 0;
while (1) {
len = read(0, &buf[i], 1);
if(len <= 0 || buf[i] == '\n') break;
i++;
}
if(i == 0) break;
buf[i] = 0;
full_argv[argc-1]=buf;
if(fork() == 0){
exec(full_argv[0], full_argv);
exit(0);
} else {
wait(0);
}
}
exit(0);
}
xv6 book