文件描述符

  1. `fd`(file descropter)通常是一个**小的非负整数**,内核用以标识一个特定进程正在访问的文件。当内核打开一个现有文件或创建一 个新文件时,它都返回一个文件描述符。在读、写文件时,可以使用这 个文件描述符。<br />`fd`:为每个正在运行的程序用文件描述符提供索引。<br />许多的Unix系统都会从文件描述符`0`读取数据,然后向文件描述符`1`写入数据。<br />0——–标准输入———-stdin<br />1——–标准输出———-stdout<br />2——–标准错误———-stderr<br />文件描述符本质上对应了内核中的一个表单数据。内核维护了每个运行进程的状态,内核会为每一个运行进程保存一个表单,表单的key是文件描述符。这个表单让内核知道,每个文件描述符对应的实际内容是什么。这里比较关键的点是,每个进程都有自己独立的文件描述符空间,所以如果运行了两个不同的程序,对应两个不同的进程,如果它们都打开一个文件,它们或许可以得到相同数字的文件描述符,但是因为内核为每个进程都维护了一个独立的文件描述符空间,这里相同数字的文件描述符可能会对应到不同的文件。<br />新分配的文件描述符总是当前进程中编号最小的未使用描述符。

I/O重定向

所谓的I/O重定向也就是让已创建的FD指向其他文件。
重定向前:
image.png
重定向后:
image.png
在I/O重定向的过程中,不变的是FD 0/1/2代表STDIN/STDOUT/STDERR,变化的是文件描述符表中FD 0/1/2对应的具体文件,应用程序只关心前者。本质上这和接口的原理是相通的,通过一个间接层把功能的使用者和提供者解耦。

把标准输出重定向到文件

和 >> 符号把标准输出重定向到文件中
> 会覆盖掉已经存在的文件中的内容
>> 则把新的内容追加到已经存在的文件中的内容的尾部 ```bash $ : > log.txt # 比如下面的命令将会把文件 log.txt 变为一个空文件(就是 size 为 0) $ ls >> log.txt # 下面的命令则把 ls 命令输出的结果追加到 log.txt 文件的尾部 $ ls 1> log.txt # 标准输出的完整写法

$ ls 2> error.txt # 标准错误的文件描述符为 2,所以重定向标准错误到文件的写法为: $ ls &> log.txt # 如果想同时把标准输出和标准错误输出重定向到同一个文件中 $ ls >log.txt 2>&1 # 标准输出和错误输出重定向到一个文件

2>&1 用到了重定向绑定,采用 & 可以将两个输出绑定在一起,也就是说错误输出将会和标准输出输出到同一个地方。

<a name="EE62Q"></a>
## read系统调用
`read(int fd,void* buf,size_t count);`
<a name="bBlN7"></a>
### 函数说明
从文件描述符`fd`对应的文件中读取数据存到`buf`缓冲区,每次读取`count`字节。同时文件指针会随着移动,read的第二个参数是指向某段内存的指针,程序可以通过指针对应的地址读取内存中的数据,这里的指针就是代码中的buf参数。
<a name="oKtCt"></a>
### 函数返回值
当返回值大于0时:实际读到的字节数

返回值=0:<br />如果读的文件:说明文件读完了<br />如果从管道或socket中读:说明对端关闭了

返回值为-1:说明发生了异常,根据`errno`的值进一步判断<br />`errno` == `EINTR` 被信号中断<br />`errno` == `EAGAIN(EWOULDBLOCK) `非阻塞方式读,并且没有数据<br />其他值 代表出现错误,可以获得返回值,然后利用`strerror(ret)`去打印错误信息。
<a name="v1JCF"></a>
## **write系统调用**
`write(int fd, void* buf, size_t count)`
<a name="IivFC"></a>
### 函数说明
把缓冲区`buf`中的前`count`个字节写入到与文件描述符`fd`相关的文件中,`write`系统调用返回的是实际写入到文件中的字节数。与读一样,`write`在当前文件偏移量处写入数据,然后将该偏移量向前推进写入的字节数:每个`write`从上一个偏移量停止的地方开始写入。
<a name="lDmDm"></a>
## open系统调用
在上面的write和read中,我们使用的文件描述符是自程序运行就有了的3个文件描述符(`0``1``2`),那么接下来open就可以创建新的文件描述符,供write和read来使用。
<a name="er0xM"></a>
### 函数说明
```c
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
    int open(const *path, int oflags);//1
    int open(const *path, int oflags, mode_t mode);//2

    exit(0);
}

函数调用方法

1.int open(const *path, int oflags);
将准备打开的文件或是设备的名字作为参数path传给函数,oflags用来指定文件访问模式。open系统调用成功返回一个新的文件描述符,失败返回-1。
其中,oflags是由必需文件访问模式和可选模式一起构成的(通过按位或“|”):
必需部分:
O_RDONLY———以只读方式打开
O_WRONLY——以只写方式打开
O_RDWR———以读写方式打开
可选部分:
O_CREAT————按照参数mode给出的访问模式创建文件
O_EXCL————–与O_CREAT一起使用,确保创建出文件,避免两个程序同时创建同一个文件,如文件存在则open调用失败
O_APPEND———-把写入数据追加在文件的末尾
O_TRUNC———–把文件长度设置为0,丢弃原有内容
将一个现有文件的内容拷贝到新文件中,新文件已存在

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    char buff[128] = {0}; //申请128字节的内存空间并初始化为0
    int out = open("file.c",O_RDONLY);//以只读方式打开文件
    int in = open("text.c",O_WRONLY);//以只写方式打开另外一个文件

    int real_num_read = read(out, buff, 128);
    while(real_num_read)//读到即写入
    {
        write(in, buff, 128);//写入的字节数为实际读到的个数
    }

    close(out);
    close(in);
    exit(0);
}

2.int open(const *path, int oflags, mode_t mode);
在第一种调用方式上,加上了第三个参数mode,主要是搭配O_CREAT使用,同样地,这个参数规定了属主、同组和其他人对文件的文件操作权限。

S_IRUSR———读权限 )
S_IWUSR———写权限 ——文件属主
S_IXUSR———-执行权限 )
S_IRGRP———-读权限 )
S_IWGRP———写权限 ——文件所属组
S_IXGRP———-执行权限 )
S_IROTH———-读权限 )
S_IWOTH———写权限——其他人
S_IXOTH———-执行权限 )
另外,也可以用文字设定法:
0——————无权限
1——————只执行
2——————只写
4——————只读

close系统调用

close(int fd);

函数用法

使用完文件描述符之后,使用close()系统调用释放!

fork系统调用

fork()可以创建一个新进程,返回子进程的PID:

//fork.c : create a new process

#include "kernel/types.h"
#include "user/user.h"

int main()
{
    int pid;

    pid = fork();

    printf("fork() retured %d\n",pid);

    if(pid == 0){
        printf("child\n");
    } else{
        printf("parent\n");
    }

    exit(0);
}

// fork()在父进程中返回子进程的PID
// 在子进程中返回0
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");
}

fork()会拷贝当前进程的内存,并创建一个新的进程,这里的内存包含了进程的指令和数据。之后,我们就有了两个拥有完全一样内存的进程。fork系统调用在两个进程中都会返回,在原始的进程中,fork系统调用会返回大于0的整数,这个是新创建进程的ID。而在新创建的进程中,fork系统调用会返回0。所以即使两个进程的内存是完全一样的,我们还是可以通过fork的返回值区分旧进程和新进程。
拷贝内存以外,fork还会拷贝文件描述符表单。
fork创建了一个新的进程。当我们在Shell中运行东西的时候,Shell实际上会创建一个新的进程来运行你输入的每一个指令。所以,当我输入ls时,我们需要Shell通过fork创建一个进程来运行ls,这里需要某种方式来让这个新的进程来运行ls程序中的指令,加载名为ls的文件中的指令(也就是后面的exec系统调用)。

Exit系统调用

exit系统调用导致调用进程停止执行并释放资源(如内存和打开的文件)。exit接受一个整数状态参数,通常0表示成功,1表示失败。

Wait系统调用

wait系统调用返回当前进程的已退出(或已杀死)子进程的PID,并将子进程的退出状态复制到传递给wait的地址;如果调用方的子进程都没有退出,那么wait等待一个子进程退出,父进程等待挂起。如果调用者没有子级,wait立即返回-1。如果父进程不关心子进程的退出状态,它可以传递一个0地址给wait。

Exec系统调用:执行

exec系统调用使用从文件系统中存储的文件所加载的新内存映像替换调用进程的内存。(百度百科:根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件)该文件必须有特殊的格式,它指定文件的哪部分存放指令,哪部分是数据,以及哪一条指令用于启动等等。
xv6使用ELF格式。当exec执行成功,它不向调用进程返回数据,而是使加载自文件的指令在ELF header中声明的程序入口处开始执行。exec有两个参数:可执行文件的文件名和字符串参数数组。例如:

char* argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv); //可执行文件的文件名,和字符串参数数组argv
printf("exec error\n");

// 程序为/bin/echo,参数列表为echo hello

子进程使用execve()加载和执行一个全新的程序,execve()会销毁现有的文本段、数据段、栈段和堆段,根据新程序的代码,创建新段来替换他们。
exec()系统调用会保留当前的文件描述符表单,在新程序中,之前的文件描述符也会对应相同的文件。
通常来说exec系统调用不会返回,因为exec会完全替换当前进程的内存,相当于当前进程已经不复存在了,所以exec系统调用没有地方可以返回。
在shell中执行,但是不想新的进程代替shell,shell就fork子进程然后exec。



#include "user/user.h"

// forkexec.c fork then exec

int 
main()
{
    int pid, status;

    pid = fork();
    if (pid == 0){ //子进程
        char *argv[] = {"echo","THIS","IS","ECHO","0"};
        exec{"echo",argv};
        printf{"exec failed\n"}; //执行未知或者错误指令,会返回退出值1
        exit(1); 
    }else {// 子进程执行完后父进程获得控制权
        printf{"parent waiting\n"};
        wait(&status}; //wait系统调用,使得父进程可以等待任何一个子进程返回,获取子进程exit参数
        printf{"The child exited with status d%\n",status};
    }
 exit(0);
}

exec将父进程的拷贝完全丢弃,有资源浪费,copy-on-write fork,这种方式会消除fork的几乎所有的明显的低效,而只拷贝执行exec所需要的内存

Shell

xv6的shell使用上述调用为用户运行程序。shell的主要结构很简单,请参见main(user/sh.c:145)。主循环使用getcmd函数从用户的输入中读取一行,然后调用fork创建一个shell进程的副本。父进程调用wait,子进程执行命令。例如:当用户向shell输入echo hello时,runcmd(user/sh.c:58) 将以echo hello为参数被调用来执行实际命令。对于“echo hello”,它将调用exec(user/sh.c:78)。如果exec成功,那么子进程将从echo而不是runcmd执行命令,在某刻echo会调用exit,这将导致父进程从main(user/sh.c:78)中的wait返回。
Xv6 隐式地分配大多数用户空间内存:fork分配父内存的子副本所需的内存,exec分配足够的内存来保存可执行文件。在运行时需要更多内存的进程(可能是malloc)可以调用 sbrk(n)将其数据内存增加n个字节; sbrk返回新内存的位置。

dup系统调用

系统调用复制一个现有的文件描述符,返回一个引用自同一个底层I/O对象的新文件描述符。两个文件描述符共享一个偏移量,就像fork复制的文件描述符一样。这是另一种将“hello world”写入文件的方法:

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

Examples

// cat -n copy.c
// 假设文件描述符已经设置好

#include "kernel/types.h"
#include "user/user.h"

int main()
{
  char buf[64];

  while(1){
    int n = read(0,buf,sizeof(buf)); // 默认情况下,fd(0)连接console的输入,fd(1)连接console的输出。 n为read读取的字符数:字符串加结束符0
    if(n<=0)
      write(1,buf,n);//从buf中向文件描述符1输出n个字节的数据
  }

  exit(0);
}

// read,write系统调用,它们并不关心读写的数据格式,它们就是单纯的读写,而copy程序会按照8bit的字节流处理数据,你怎么解析它们,完全是用应用程序决定的。所以应用程序可能会解析这里的数据为C语言程序,但是操作系统只会认为这里的数据是按照8bit的字节流。
// open.c : create a file,write to it.

#include "kernel/types.h"
#include "user/user.h"
#include "kernel/fcntl.h"

int main()
{
    int fd = open("output.txt", O_WRONLY | O_CREATE);
    write(fd,"ooo\n",4);

    exit(0)
}

系统调用 描述
int fork() 创建一个进程,返回子进程的PID
int exit(int status) 终止当前进程,并将状态报告给wait()函数。无返回
int wait(int *status) 等待一个子进程退出; 将退出状态存入*status; 返回子进程PID。
int kill(int pid) 终止对应PID的进程,返回0,或返回-1表示错误
int getpid() 返回当前进程的PID
int sleep(int n) 暂停n个时钟节拍
int exec(char file, char argv[]) 加载一个文件并使用参数执行它; 只有在出错时才返回
char *sbrk(int n) 按n 字节增长进程的内存。返回新内存的开始
int open(char *file, int flags) 打开一个文件;flags表示read/write;返回一个fd(文件描述符)
int write(int fd, char *buf, int n) 从buf 写n 个字节到文件描述符fd; 返回n
int read(int fd, char *buf, int n) 将n 个字节读入buf;返回读取的字节数;如果文件结束,返回0
int close(int fd) 释放打开的文件fd
int dup(int fd) 返回一个新的文件描述符,指向与fd 相同的文件
int pipe(int p[]) 创建一个管道,把write/read文件描述符放在p[0]和p[1]中
int chdir(char *dir) 改变当前的工作目录
int mkdir(char *dir) 创建一个新目录
int mknod(char *file, int, int) 创建一个设备文件
int fstat(int fd, struct stat *st) 将打开文件fd的信息放入*st
int stat(char file, struct stat st) 将指定名称的文件信息放入*st
int link(char file1, char file2) 为文件file1创建另一个名称(file2)
int unlink(char *file) 删除一个文件

表1.2:xv6系统调用(除非另外声明,这些系统调用返回0表示无误,返回-1表示出错)

char buf[512];
int n;
for (;;) {
    n = read(0, buf, sizeof buf);
    if (n == 0)
        break;
    if (n &lt; 0) {  // &lt; 是 <  if (n < 0)
        fprintf(2, "read error\n");
        exit(1);
    }
    if (write(1, buf, n) != n) {
        fprintf(2, "write error\n");
        exit(1);
    }
}