系统调用

在操作系统的简介中,说到为了方便程序使用硬件资源操作系统会提供一些应用接口。

如Figure1.1所示,在xv6中采取一个内核(kernel)空间的设计:一个特殊的程序提供服务给运行的程序。每个运行的程序我们称作为进程(Process)拥有指令、数据、栈。用户空间(user space): 抽象出的一个空间,用于用户运行应用程序,如shell(本质上是一个应用程序,功能就是读取命令并执行)、cat

  • 指令(instruction):实现程序的计算
  • 数据(data):一些计算的变量
  • 栈(stack):组织的程序(C语言中的函数)的调用

当进程调用内核服务时我们就需要进行(invoke)系统调用,系统调用会进入内核,然后内核执行服务并返回,于是这样的一个进程会选择在用户空间与内核空间执行。

image.png

进程与内存

进程(Process):

进程的概念:进程简单来说就是正在运行的程序,例如我们以及启动的word办公软件、游戏应用等应用程序。

我们为了实现一个系统能够同时运行多个程序,系统需要提供一种拥有多个CPU的假象,称作为虚拟化CPU

那为了实现这种技术我们采用了一种叫做时分(time sharing) 的CPU技术:一个进程只会运行一个时间片,之后便切换到其他进程,从而提供多个CPU的假象。

进程之间的切换称作为上下文切换(context switch).

进程系统调用:xv6

进程API设计所需要包含的内容

  • 创建(create):操作系统必须包含一些创建新进程的方法。在 shell 中键入命令或双击应用程序图标时,会调用操作系统来创建新进程,运行指定的程序。
  • 销毁(destroy):由于存在创建进程的接口,因此系统还提供了一个强制销毁进程的接口。
  • 等待(wait):有时等待进程停止运行是有用的,因此经常提供某种等待接口。
  • 其他控制(miscellaneous control):除了杀死或等待进程外,有时还可能有其他控制。
  • 状态(status):通常也有一些接口可以获得有关进程的状态信息。
系统调用 描述
int exit(int status) 中止当前进程,将状态报告给wait(),没有返回
int getpid() 返回当前进程的PID
int fork() 创建进程,返回子进程的PID
int wait(int *status) 父进程等待子进程退出(exit);退出状态是*Status,返回值为子进程的PID
int sleep(int n) 暂停进程n个时钟ticks的时间
int exec(char *file, char *argv[]) 加载文件(*file)执行,以其参数(*argv)执行程序,错误后返回-1
int kill(int pid) 中止进程号为PID的进程,成功返回0,错误返回-1
char *sbrk(int n) 增加当前进程的内存空间,并返回新内存的开始位置

应用示例

  1. 创建
    使用fork()系统调用,可以创建进程,新创建的进程(child)会复制当前进程(parent)的内存(数据与指令)。fork()创建进程成功后(不同的进程的PID不会相同),子进程的返回值为0,父进程的返回值为子进程的PID

  2. 退出—-其他控制
    使用exit()系统调用,会导致调用它的进程停止运行,并且释放诸如内存和打开文件在内的资源。

  3. 等待
    使用wait()系统调用,会返回一个当前进程已退出的子进程,如果没有子进程退出,wait 会等候直到有一个子进程退出。如果调用该接口的进程没有子进程,会返回-1,当我们不在意子进程的状态时,我们可以传入0去等待。

观看下述代码示例

1
2
3
4
5
6
7
8
9
10
11
12
int pid = fork();

if(pid > 0){
printf("parent: child=%d\n", pid);
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");
}

上述示例中的运行结果为

1
2
3
4
parent: child=32381
child: exiting
//这条语句是等待子进程退出后才会执行
child 32381 is done

NOTE: 虽然我们父子进程在初始化时会有相同的内存,但是在之后,他们会有单独的寄存器与内存,两个进程的变量彼此互不干扰。例如,对于变量PID,父进程中PID = 32381,而子进程的PID = 0

  1. 执行文件
    使用exec系统调用 ,将从某个文件(通常是可执行文件)里读取内存镜像,并将其替换到调用它的进程的内存空间。

示例:这段代码将调用程序替换为 /bin/echo 这个程序,这个程序的参数列表为echo hello。大部分的程序都忽略第一个参数,这个参数惯例上是程序的名字(此例是 echo)。

1
2
3
4
5
6
char *argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error\n");

xv6: user/sh.c中我们会开启一个死循环进行读取用户的输入,我们会创建一个进程,将解析完成的参数,传递给runcmd()函数去调用exce执行程序(echo程序),wait是主程序需要等待f子进程的退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
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
//+3("cd ") meanning skip to the string for dir
if(chdir(buf+3) < 0)
fprintf(2, "cannot cd %s\n", buf+3);
continue;
}
if(fork1() == 0)
runcmd(parsecmd(buf));
wait(0);
}

xv6 通常隐式地分配用户的内存空间。fork 在子进程需要装入父进程的内存拷贝时分配空间,exec 在需要装入可执行文件时分配空间。一个进程在需要额外内存时可以通过调用 sbrk(n) 来增加 n 字节的数据内存。 sbrk 返回新的内存的地址。

进程创建(细节)

  1. 将代码和静态数据(初始化变量)加载到内存,再加载到进程的地址空间。(操作系统读取硬盘程序字节,并将其读入到内存中)
  2. 为程序运行期栈(run-time stack)分配内存,在C语言中我们使用栈来存放局部变量、函数参数和返回地址。操作系统可以使用参数来初始化栈,例如在linux中我们将argc、argv参数填入main中
  3. 为程序的堆(heap)分配内存,如C语言中使用malloc与free,申请与释放的内存为堆内存。注:堆内存远大于栈内存,因此一些大型数据结构需要使用堆来申请内存。
  4. 执行一些初始化任务,如一些IO操作。在unix中,每个进程默认情况都会打开3个文件描述符(file descriptor,一个整数句柄),用于标准输入输出和错误。

OSTEP4.1

进程的数据结构(xv6)

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
// Saved registers for kernel context switches.
struct context {
uint64 ra;
uint64 sp;

// callee-saved
uint64 s0;
uint64 s1;
uint64 s2;
uint64 s3;
uint64 s4;
uint64 s5;
uint64 s6;
uint64 s7;
uint64 s8;
uint64 s9;
uint64 s10;
uint64 s11;
};

enum procstate { UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
// Per-process state
struct proc {
struct spinlock lock;

// p->lock must be held when using these:
enum procstate state; // Process state
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID

// wait_lock must be held when using this:
struct proc *parent; // Parent process

// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
};

I/O 与 文件描述符

简介:

  • I/O: 输入(input)与输出(output),C语言的头文件<stdio.h>,代表的就是标准输入与输出
  • 文件描述符: file descriptor,用一个整数来表示内核管理的对象,进程可以通过该描述符进行读或写,文件描述符接口将这些文件、管道、设备抽象为字节流

获得文件描述符(fd),可以通过打开文件、目录、或设备、创建一个管道(Pipe)、通过复制(duplicate)已经存在的文件描述符

  • 进程表: 在xv6中每个进程都会有一个进程表,而文件描述符就是作为其中的一个索引。因此每个进程的私有空间内的文件描述符都是以0开始。

在xv6中,通常情况下,进程会默认定义了3个文件描述符。

  • 标准输入:0,将fd=0文件的内容进行读取,也就是读取命令行的内容,等同于C语言中的stdin
  • 标准输出:1,将内容输出写入到fd=1的文件中,也就是输出内容到命令行,等同于stdout
  • 错误信息:2,将错误信息输出到fd=2的文件中,等同于stderr

fd = 0,1,2,这三个描述符引用的都是同一个文件console,相当于复制(duplicate)的操作,将三个描述符用于不同的路径。

1
2
3
4
5
6
while((fd = open("console", O_RDWR)) >= 0){
if(fd >= 3){//我们会陆续获得到0,1,2三个描述,为3时破除循环
close(fd);
break;
}
}

I/O系统调用:xv6

系统调用 描述
int open(char *file, int flags) 打开一个文件,指定读写的标识(flag),返回文件描述符
int write(int fd, char *buf, int n) 将buf的内容写入fd引用的文件,返回值为n
int read(int fd, char *buf, int n) 将fd的文件内容读取到buf中,返回读取的数量,如果为0这代表EOF
int close(int fd) 释放打开的文件描述符,包括open、pipe、dup所产生的fd,释放后可以复用该fd
int dup(int fd) 返回一个新的描述符并引用与fd相同的文件
int pipe(int p[]) 创建管道,将标准输入输出描述符放入p[0]p[1]

open()接口的第二个参数为标识符(打开文件的方式):

  • O_RDONLY:只读
  • O_WRONLY:只写
  • O_RDWR:读写
  • O_CREATE:指定的文件不存在,新建文件
  • O_TRUNC:截断,将文件内容清除

应用示例

  • cat应用:文件位置user/cat.c,我们将open、read、write三个系统接口接口聚合形成了cat这个打印文件内容的程序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    char buf[512];
    int main(int argc,char *agrv[]){
    ...
    for(i = 1; i < argc; i++){
    if((fd = open(argv[i], 0)) < 0){
    fprintf(2, "cat: cannot open %s\n", argv[i]);
    exit(1);
    }
    cat(fd);
    close(fd);
    }
    ...
    }

void cat(int fd){

while((n = read(fd, buf, sizeof(buf))) > 0) {
if (write(1, buf, n) != n) {
fprintf(2, “cat: write error\n”);
exit(1);
}
}

}

1
2
3
例如我们使用cat打印hello.txt,文件内容为hello world!
```shell
cat hello.txt
  1. 首先在main中使用open打开hello.txt,第二个参数0代表只读(kernel / fctrl.c)中定义了O_RDONLY为0x000
  2. 然后将文件描述符传递给cat函数,根据指定的文件描述符使用read接口读取hello.txt的内容
  3. 最后再通过write将缓冲区的内容,写入到标准输出中

I/O重定向

I/O重定向: 通常情况我们都是将内容输出到屏幕也就是命令行,通过I/O重定向便可以将内容输出到指定文件内。I/O的重定向 = 将标准输入、输出、错误信息打印,三个文件描述符替换,作为指定文件的描述符

linux下的重定向符号使用

  • 命令 > 文件:将内容输出到指定文件
  • 命令 2> 文件,可以将错误信息输出到指定文件
  • 命令 >> 文件:以追加的形式输出到指定文件
  • 命令 > 文件 2>&1:将标准输出于错误消息输出到指定文件,在后续将会使用到该符号,等同于命令 &> 文件。个人理解:&作为引用相当于duplicate,fd=2作为一个fd=1的副本。

    1
    $ ls a b > tmp1 2>&1 #b目录不存在,将2(标准错误打印)重定向1(标准输出)
  • 命令 < 文件:将文件内容输入到指定位置,将文件内容作为命令的标准输入
  • 命令 < 文件1 > 文件2:将文件1作为命令的标准输入,并将标准输出到文件2
1
2
3
4
5
6
7
8
----打印到屏幕上----
$ echo hello world!
----写入hello.txt文件中----
$ echo hello world! > hello.txt
----使用重定向清空文件内容,原理是将空内容输出到指定文件内----
$ : > hello.txt # or true > hello.txt
$ cat /dev/null > hello.txt
$ echo > hello.txt

实现: fork() + 文件描述符

在进程讲解中我们知道了,fork生成的子进程会拷贝父进程的内存,那么父进程的描述符同样会拷贝到子进程。exec()系统调用将会替代调用进程的内存(后续不再执行该进程),但是会保留文件表。

扩展: 在命令行键入cat,不加上文件名。cat会采用标准输入(fd=0)作为输入流运行程序。键入cat后,cat程序会一直占用标准输入,我们输入一行命令,就会输出一行命令。

由于所有进程默认打开了三个文件描述符0,1,2。因此使用open打开文件的返回值是3。要想单个cat命令能够获取文件内容,我们就需要使用IO重定向。那么我们通过使用fork+exec两个接口来实现重定向,以cat < input.txt为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(){
char *argv[2];
argv[0] = "cat";
argv[1] = 0;
if(fork() == 0) {
close(0);
int fd = open("input.txt", O_RDONLY);
fprintf(stdout,"fd = %d\n",fd);
execvp( "cat", argv);
}
}

将当前的标准输入(fd = 0)替换为需要读入的文件描述符,也就是关闭完标准输入(fd = 0)后,此时调用open,我们会发现这个文件的描述符为0。这时我们再使用单个cat命令,只会读取我们打开的文件的内容。

为什么我们需要将exec与fork两个接口分开

子进程进行IO重定向,不会影响到主进程的IO设置。根据cat重定向,根据上述程序,若我们只使用一个forkexec,没有更换标准文件描述符的机会,就不可能实现IO重定向的功能。

硬件接口占用问题

虽然父子进程拥有隔离的文件描述符表,但是父子进程共用相同的文件偏移量。也就是当父子进程都使用同一个文件描述符时,我们无法控制其运行的顺序。例如:父子进程同时使用了标准输出的描述符,若没有wait,会出现world\n hello的输出

1
2
3
4
5
6
7
if(fork() == 0) {
write(1, "hello ", 6);
exit(0);
} else {
wait(0);
write(1, "world\n", 6);
}

在上述程序中我们使用了wait(0),让父进程去等待子进程退出,从而控制了执行顺序。

dup接口使用

上述程序使用了dup接口进行改写,复制的fd引用了标准输出(fd=1)

1
2
3
fd = dup(1);
write(1, "hello ", 6);
write(fd, "world\n", 6)

细节

xv6中的IO重定向(user/sh.c)

1
2
3
4
5
6
7
8
case REDIR:
rcmd = (struct redircmd*)cmd;
close(rcmd->fd);
if(open(rcmd->file, rcmd->mode) < 0){
fprintf(2, "open %s failed\n", rcmd->file);
exit(1);
}
runcmd(rcmd->cmd);//使用exce

管道(pipe)

简介:

管道: 是一个小的内核缓冲区提供给进程,作为一对文件描述符。一个用于写,一个用于读。给两个程序中创建一个通讯的管道,能够进行数据交换。

作用: 用于不同进程之间的通讯,省去了临时文件的创建于删除。

示例

该程序wc(wordcount),我们将标准输入连接到了管道的读取端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int p[2];
char *argv[2];

argv[0] = "wc";
argv[1] = 0;

pipe(p);
if(fork() == 0) {
close(0);
dup(p[0]);// 复制一个 p[0](管道读端),此时文件描述符 0(标准输入)也
//引用管道读端,故改变了标准输入。
close(p[0]);//关闭了标准输入,同时p[0]管道端也失效
close(p[1]);//防止子进程自己阻塞自己
exec("/bin/wc", argv);// wc 从标准输入读取数据(同cat),并写入到
// 参数中的每一个文件
} else {
close(p[0]);
write(p[1], "hello world\n", 12);
close(p[1]);
}

在该程序中我们调用了pipe()系统调用,我们创建了新的管道,在p中记录下了文件的读写描述符。父子进程都标准IO引用同一管道

  • 子进程中我们close了标准输入(fd=0)的描述符,使用dup复制了管道输入描述符,从而引用了管道的读取(输入)端。简单来说就是将标准输入替换成了管道输入也就是重定向。
  • 父进程中我们关闭了标准读取(输入)管道,将文件内容写入标准写入(输出)管道,从而将内容写入pipe缓冲区

管道数据流向图

pipe

父进程比子进程慢一点执行的话,缓冲区没有数据,会不会导致子进程读取失败?

缓冲区没有数据,会有以下三种情况:

  • 等待写入(输出)端写入数据,输入端再进行读取(阻塞)

  • 等待引用写入(输出)端的管道描述符被关闭

在执行exec程序前,执行close(p[1]),若注释该语句,我们将不会有输出。
wc 的一个fd仍然引用了管道的写端,那么 wc 将永远看不到文件的关闭(被自己阻塞)。

  • 读取为0,就像读取到EOF一样

用途

在xv6中shell实现了管道,如grep fork sh.c | wc -l。创建两个子进程分别执行,将左端grep输出的内容,发送给了右端wc作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
case PIPE:
pcmd = (struct pipecmd*)cmd;
if(pipe(p) < 0)
panic("pipe");
if(fork1() == 0){
close(1);
dup(p[1]);
close(p[0]);
close(p[1]);
runcmd(pcmd->left);
}
if(fork1() == 0){
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
runcmd(pcmd->right);
}
close(p[0]);
close(p[1]);
wait(0);
wait(0);
break;

xv6-pipe

如图:

  1. 创建两个子进程(c1、c2),创建管道连接左端(写入端)和右端(读取端)
  2. 左端写入端: 子进程1中,运行runcmd,命令如grep fork sh.c,将内容写入buffer
  3. 读取端: 子进程2中,运行runcmd,如wc -l,读取buffer的数据
  4. 互相等待其完成,简单来说一写一读

右端可以包含一个或多个命令,如a|b|c,其中a为写入端,b、c都是读取端,b、c都是两个子进程

Pipe的实际作用

下述示例中管道除了没有临时文件产生没有什么用途了

1
2
3
4
#pipe
$ echo hello world | wc
#common:without pipe
$ echo hello world >/tmp/xyz; wc </tmp/xyz

但是管道与临时文件相比也有以下优势

  1. 管道会自动清理自己;如果是文件重定向,shell 在完成后必须小心翼翼地删除/tmp/xyz。
  2. 第二,管道可以传递任意长的数据流,而文件重定向则需要磁盘上有足够的空闲空间来存储所有数据。
  3. 管道可以分阶段的并行执行,而文件方式则需要在第二个程序开始之前完成第一个程序。
  4. 如果你要实现进程间的通信,管道阻塞读写比文件的非阻塞语义更有效率。

文件系统

简介

文件系统

文件系统

操作系统中用于管理磁盘的软件,可以高效的将用户创建的文件通过I/O设备存储到系统的磁盘中,从而实现持久化地存储数据。

文件类型

  • 目录:保存了数据文件与其他目录的名称引用,目录本质上也是文件
  • 数据文件:连续的字节数组
  • 设备文件:所有硬件都有对应的文件,文件系统通过文件去访问特定的设备。什么是设备文件? - 知乎

文件存储路径

目录和文件将会形成一个树状结构(tree)。

/ 称为根(root)目录是所有文件的开始目录,以/ 开始表示的路径称为绝对路径 ,如/a/b/c,代表访问到了c这个文件或者是目录。反之不是以/开始的路径称为的相对路径

file_tree

文件结构

Linux 文件系统会为每个文件分配两个数据结构:索引节点(index node)目录项(directory entry),它们主要用来记录文件的元信息和目录层次结构。

索引节点(inode)

文件系统会给硬盘内的每个文件分配一个底层数据结构inode,每一个inode的编号对应一个文件,用于保存文件元信息。在linux下我们可以使用stat命令查看inode消息,inode数据结构存在于磁盘中。

inode中的元信息(metadata)

  • 索引节点ID:对应文件本身
  • 文件的字节数
  • 文件类型:数据文件、目录、设备文件
  • 链接数,即有多少文件名指向这个inode
  • 文件内容在磁盘的位置

目录项(directory entry) = dentry

用来记录文件的名字、inode指针以及与其他dentry的层级关联关系。多个dentry关联起来,就会形成目录结构,但它与inode不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存。

由于inode唯一标识一个文件,而dentry记录着文件的名称,所以目录项和索引节点的关系是多对一,也就是说,一个文件可以有多个名称。

硬盘存储数据

磁盘读写的最小单位是扇区,扇区的大小只有 512B 大小,如果每次读写都以这么小为单位,那这读写的效率会非常低。

所以,文件系统把多个扇区组成了一个逻辑块,每次读写的最小单位就是逻辑块(数据块),Linux 中的逻辑块大小为 4KB,也就是一次性读写 8 个扇区,这将大大提高了磁盘的读写的效率。

以上就是索引节点、目录项以及文件数据的关系,下面这个图就很好的展示了它们之间的关系:

inode and entry

inode是存储在硬盘上的数据,那么为了加速文件的访问,通常会把inode加载到内存中

参考:小林coding

文件系统调用:xv6

系统调用 描述
int chdir(char *dir) Change the current directory.
int mkdir(char *dir) Create a new directory.
int mknod(char *file, int, int) Create a device file.
int fstat(int fd, struct stat *st) Place info about an open file into *st.
int stat(char *file, struct stat *st) Place info about a named file into *st.
int link(char *file1, char *file2) Create another name (file2) for the file file1.
int unlink(char *file) Remove a file.
  • cddir: 切换当前文件目录,同cd命令

  • mkdir: 创建目录,同linux下的mkdir命令

  • mknod: 创建一个特殊的文件,对设备文件进行引用,与设备文件相关的是主要和次要设备编号(mknod的两个参数),它们唯一地标识内核设备。

  • link:创建一个文件名,对相同的inode进行引用作为一个存在的文件

    应用示例

  • chdir与open

    1
    2
    3
    4
    5
    6
    /*---使用chdir改变当前目录,以相对路径创建文件---*/
    chdir("/a");
    chdir("b");
    open("c", O_RDONLY);
    /*---以绝对路径创建文件---*/
    open("/a/b/c", O_RDONLY);
  • mkdir与mknod

    1
    2
    3
    4
    5
    6
    /*在根目录下创建dir文件夹,并以只写的方式创建file文件*/
    mkdir("/dir");
    fd = open("/dir/file", O_CREATE|O_WRONLY);
    close(fd);
    /*创建console设备文件*/
    mknod("/console", 1, 1);

    创建设备文件后,内核回将读写的系统调用转移到内核设备,而不是传递给文件系统

  • link与unlink

    链接(link): 每个链接由目录中的条目也就是(dentry)组成,dentry内容包含文件名与引用的inode

如下,创建b的文件进行读写和对a进行读写的效果相同,对a、b文件使用stat命令进行观察,可以观测到a、b有相同的inode号与一样的链接数(nlink) =2。当使用unlink后,我们再观测b文件会返现nlink=0

1
2
3
4
5
/*创建一个文件a,创建b文件对a进行链接*/
open("a", O_CREATE|O_WRONLY);
link("a", "b")
/*取消a文件的链接*/
unlink("a");

数据结构(xv6)

  • fstat系统调用检索inode的文件描述符信息,并将其填入stat结构体中如下所示:

    1
    2
    3
    #define T_DIR     1   // Directory
    #define T_FILE 2 // File
    #define T_DEVICE 3 // Device

struct stat {
int dev; // File system’s disk device
uint ino; // Inode number
short type; // Type of file
short nlink; // Number of links to file
uint64 size; // Size of file in bytes
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- cd命令改变shell当前的工作目录,如果cd作为普通命令执行,那么我们将会fork一个子进程,而子进程只会改变当前目录,不会该改变父进程目录。以此不需要fork一个进程去修改目录。
```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
//buf+3("cd ") '+3'meanning skip to the string for dir
if(chdir(buf+3) < 0)
fprintf(2, "cannot cd %s\n", buf+3);
continue;
}
if(fork1() == 0)
runcmd(parsecmd(buf));
wait(0);
}

总结

本章主要是讲解了系统调用的一些接口,并且简单的介绍了一些linux的常识。本节课也有对应的实验,可以让你去实现一些常用linux的工具lab1(其中十分有趣的实验用pipe去实现流水线)。