xv6的系统接口是类Unix的,此篇文章是对xv6-book的Chapter0内容的总结
Shell执行命令的过程
简单看一下shell是怎么执行echo “hello world”这样的命令的
在shell的main函数(sh.c)中可以看到如下循环:
// Read and run input commands.
while(getcmd(buf, sizeof(buf)) >= 0){
if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
// Chdir must be called by the parent, not the child.
buf[strlen(buf)-1] = 0; // chop \n
if(chdir(buf+3) < 0)
printf(2, "cannot cd %s\n", buf+3);
continue;
}
if(fork1() == 0)
runcmd(parsecmd(buf));
wait();
}
getcmd获取用户的一行命令,这里先不管chdir的问题,可以看到shell执行了fork1函数,即建立了一个新进程,紧接着调用runcmd函数去在子进程中执行相应的程序,对于echo “hello world”来说,runcmd就要使用exec系统调用去执行echo程序了,相当于执行如下代码
char *argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error\n");
而对于chdir来说,之所以要写入shell的代码里,是因为chdir对应的cd命令要改变的是shell本身的文件路径,如果作为用户程序执行的话,流程就是先fork一个子进程,然后在子进程里执行cd,这样显然不可行了
文件描述符
默认情况下文件描述符0表示标准输入,1表示标准输出,2表示错误
read(fd, buf, n)表示从文件描述符fd读取最多n个字节,并拷贝到buf中,并且返回读到的字节数,每个引用某个文件的文件描述符都会记录一个读取的offset,每次read都会增加这个offset,直到读到EOF,此时read返回0
write(fd, buf, n)从buf向文件fd中写入n个字节,每次write也有个相应的offset。write如果写入不足n个字节,则返回错误
如下代码展示了cat的原理,它从标准输入拷贝数据,输出到标准输出,如果出错的话输出错误信息到标准错误
char buf[512];
int n;
for(;;){
n = read(0, buf, sizeof buf);
if(n == 0)
break;
if(n < 0){
fprintf(2, "read error\n");
exit();
}
if(write(1, buf, n) != n){
fprintf(2, "write error\n");
exit();
}
}
如下代码中,子进程中先关闭了标准输入,接着打开了input.txt,由于文件描述符的分配是最小可用原则,所以此时input.txt就成了标准输入,完成了IO重定向
char *argv[2];
argv[0] = "cat";
argv[1] = 0;
if(fork() == 0) {
close(0);
open("input.txt", O_RDONLY);
exec("cat", argv);
}
且在xv6系统中使用的重定向原理也就是上述代码的思想,相关代码如下
case REDIR:
rcmd = (struct redircmd*)cmd;
close(rcmd->fd);
if(open(rcmd->file, rcmd->mode) < 0){
printf(2, "open %s failed\n", rcmd->file);
exit();
}
runcmd(rcmd->cmd);
break;
虽然fork子进程的时候复制了父进程的文件描述符表,但是每个文件描述符对应的read和write的offset是相同的,考虑如下代码,可以看到父子进程都向标准输出写,最后输出的是“hello world”,也就是说父进程的写入刚好写在了子进程的后面,说明了其共享offset
if(fork() == 0) {
write(1, "hello ", 6);
exit();
} else {
wait();
write(1, "world\n", 6);
}
dup系统调用可以复制一个文件描述符,与fork时复制文件描述符相同,复制的描述符和原始的描述符也共享offset,比如如下代码也可以写入“hello world”
fd = dup(1);
write(1, "hello ", 6);
write(fd, "world\n", 6);
Pipe管道
主要注意pipe的read端在没有数据时会等待数据的写入,如果此时这个pipe的写端文件描述符都被关闭了的话,那么此时read也会返回(收到EOF)
文件系统
chdir系统调用相当于cd命令,比如如下代码块中的两段代码其功能相同
//1
chdir("/a");
chdir("b");
open("c", O_RDONLY);
//2
open("/a/b/c", O_RDONLY);
mkdir可以创建一个新的目录,而带有O_CREATE的open调用创建新的文件,而mknod创建新的设备文件,设备文件被打开时会把这个文件的read和write等系统调用转换为对设备驱动程序的调用(也就是重新实现的read和write函数)
mkdir("/dir");
fd = open("/dir/file", O_CREATE|O_WRONLY);
close(fd);
mknod("/console", 1, 1);
fstat函数可以返回指定文件描述符所指向文件的状态(也就是struct stat的内容),代码如下
#define T_DIR 1 // Directory
#define T_FILE 2 // File
#define T_DEV 3 // Device
struct stat {
short type; // Type of file
int dev; // File system’s disk device
uint ino; // Inode number
short nlink; // Number of links to file
uint size; // Size of file in bytes
};
文件名不等于文件,文件底层是inode,而文件名只是个链接,比如如下代码段创建了一个文件,它有两个名字a和b,此时上述struct stat中的nlink就是2,但是只有一个inode指向(即ino)
open("a", O_CREATE|O_WRONLY);
link("a", "b");
unlink可以撤销一个文件名(link),而当nlink减为0时,该文件就会被从磁盘上清除。比如可以在程序里创建临时文件(如下代码),这样当程序退出时,文件就会被从磁盘上清除
fd = open("/tmp/xyz", O_CREATE|O_RDWR);
unlink("/tmp/xyz");