Appearance
MIT 6.S081 Lab Utilities 补充内容
- 系统调用:fork、exit、wait、exec
- 文件描述符、read & write
- pipe
- 文件系统
系统调用
基本概念
kernel(内核):为运行的程序提供服务的一种特殊程序。每个运行着的程序叫做进程,每个进程的内存中存储指令、数据和堆栈。一个计算机可以拥有多个进程,但是只能有一个内核。
每当进程需要调用内核时,它会触发一个system call(系统调用),system call进入内核执行相应的服务然后返回。
shell:一个普通的程序,其功能是让用户输入命令并执行它们,shell不是内核的一部分。
每个进程拥有自己的用户空间内存以及内核空间状态,当进程不再执行时xv6将存储和这些进程相关的CPU寄存器直到下一次运行这些进程。kernel将每一个进程用一个PID(process identifier)指代。
常用的system call有:fork
、exit
、wait
和exec
。
fork
- 形式:
int fork();
- 作用:让一个进程生成另外一个和这个进程的内存内容相同的子进程
- 返回值
- 在父进程中,返回值是这个子进程的PID
- 在子进程中,返回值是0
- 如果出现错误,返回一个负值
我们可以通过fork返回的值来判断当前进程是子进程还是父进程。(注: fork 调用生成的新进程与其父进程谁先执行不一定,哪个进程先执行要看系统的进程调度策略)
为什么父子进程中返回值不同?因为这相当于是链表,返回值就是指向子进程的指针,在当次fork时,父进程返回值指向子进程,而子进程刚创建还没有子进程所以它的返回值是0。
用通俗的语言来说,fork就是一个进程在执行的过程中创建了一个和自己一模一样的分身两个进程一起往下执行。
exit
- 形式:
int exit(int status);
- 作用:让调用它的进程停止执行并且将内存等占用的资源全部释放
- 参数:需要一个整数形式的状态参数,0代表以正常状态退出,1代表以非正常状态退出
wait
- 形式:
int wait(int *status);
- 作用:等待子进程退出
- 参数:子进程的退出状态存储到
int *status
这个地址中 - 返回值
- 返回子进程PID
- 如果没有子进程返回-1
举例
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");
}
前两行的输出:
parent: child=1234
child: exiting
或
child: exiting
parent: child=1234
因为父子进程的执行顺序不能确定。
最后一行输出:
parent: child 1234 is done
子进程在判断完pid == 0
之后将exit
,父进程发现子进程exit
之后,wait
执行完毕,打印输出。
尽管fork
了之后子进程和父进程有相同的内存内容,但是内存地址和寄存器是不一样的,也就是说在一个进程中改变变量并不会影响另一个进程。
exec
- 形式:
int exec(char *file, char *argv[]);
- 作用:加载一个文件,获取执行它的参数,执行
- 返回值:执行错误返回-1,执行成功则不会返回
xv6 shell如何执行程序
在shell进程的main
中主循环先通过getcmd
来从用户获取命令,然后调用fork
来运行一个和当前shell进程完全相同的子进程。父进程调用wait
等待子进程exec
执行完(在runcmd
中调用exec
)。
/* sh.c */
int
main(void)
{
static char buf[100];
int fd;
// Ensure that three file descriptors are open.
while((fd = open("console", O_RDWR)) >= 0){
if(fd >= 3){
close(fd);
break;
}
}
// 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)
fprintf(2, "cannot cd %s\n", buf+3);
continue;
}
if(fork1() == 0)
runcmd(parsecmd(buf));
wait(0);
}
exit(0);
}
I/O和文件描述符
File Descriptors
file descriptor:文件描述符,用来表示一个被内核管理的、可以被进程读/写的对象的一个整数,表现形式类似于字节流,通过打开文件、目录、设备等方式获得。一个文件被打开得越早,文件描述符就越小。
每个进程都拥有自己独立的文件描述符列表,其中0是标准输入,1是标准输出,2是标准错误。shell将保证总是有3个文件描述符是可用的。
read
- 形式:
int read(int fd, char *bf, int n);
- 作用:从文件描述符
fd
读n字节bf
的内容 - 返回值:返回值是成功读取的字节数
每个文件描述符有一个offset,read
会从这个offset开始读取内容,读完n个字节之后将这个offset后移n个字节,下一个read
将从新的offset开始读取字节。
举例:从标准输入读入一个字符
char bf;
if(read(0, &bf, 1) < 0){
fprintf(2, "read error\n");
exit(1);
}
write
- 形式:
int write(int fd, char *bf, int n);
- 作用:向文件描述符
fd
写n字节bf
的内容 - 返回值:返回值是成功写入的字节数
write
也有类似read
的offset。
举例:向标准输出写入一个字符
char bf;
if(write(1, bf, 1) != 1){
fprintf(2, "write error\n");
exit(1);
}
close
- 形式:
int close(int fd)
- 作用:将打开的文件
fd
释放,使该文件描述符可以被后面的open
、pipe
等其他system call使用。
举例:使用close
来修改file descriptor table能够实现I/O重定向
/* implementation of I/O redirection,
* more specifically, cat < input.txt
*/
char *argv[2];
argv[0] = "cat";
argv[1] = 0;
if (fork() == 0) {
// in the child process
close(0); // this step is to release the stdin file descriptor
open("input.txt", O_RDONLY); // the newly allocated fd for input.txt is 0, since the previous fd 0 is released
exec("cat", argv); // execute the cat program, by default takes in the fd 0 as input, which is input.txt
}
父进程的fd table将不会被子进程fd table的变化影响,但是文件中的offset将被共享。
dup
- 形式:
int dup(int fd);
- 作用:复制一个新的
fd
指向的I/O对象,返回这个新fd值,两个I/O对象(文件)的offset相同
举例:
fd = dup(1);
write(1, "hello ", 6);
write(fd, "world\n", 6);
// outputs hello world
除了dup
和fork
之外,其他方式不能使两个I/O对象的offset相同,比如同时open
相同的文件。
Pipe
基本概念
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
管道能够实现进程间的通信,是一种半双工通信(可以选择方向的单向通信)。
pipe:管道,暴露给进程的一对文件描述符,一个文件描述符用来读,另一个文件描述符用来写,将数据从管道的一端写入,将使其能够被从管道的另一端读出。
使用方法
pipe
也是一个system call,形式为int pipe(int p[])
,p[0]
为读取的文件描述符,p[1]
为写入的文件描述符。
举例:父进程写子进程读
- 父进程创建管道,得到两个⽂件描述符指向管道的两端
- 父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。
- 父进程关闭fd[0],子进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。
int fd[2];
char bf = 'a';
pipe(fd);
if(fork() == 0){
close(fd[1]);
read(fd[0], &bf, 1);
printf("received\n");
close(fd[0]);
} else {
close(fd[0]);
write(fd[1], &bf, 1);
close(fd[1]);
}
特点
- 管道只允许具有血缘关系的进程间通信,如父子进程间的通信。
- 管道只允许单向通信。
- 管道内部保证同步机制,从而保证访问数据的一致性。
- 面向字节流
- 管道随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消失。
注意:
- 写满管道再写,且读端未全部关闭,write阻塞
- 读空管道,且写端未全部关闭,
- 而当它们的引用计数等于0时,只写不读会导致写端的进程收到一个SIGPIPE信号,导致进程终止,只写不读会导致read返回0,就像读到⽂件末尾⼀样