CSAPP基本版第十章学习日志:关于系统级I/O

输入/输出(I/O)以及系统调用封装函数

输入/输出(I/O) 是在主存和外部设备(例如磁盘驱动器、终端和网络)之间复制数据的过程。输入操作是从I/O设备复制数据到主存,而输出操作是从主存复制数据到I/O设备。
通常,将键盘和显示器构成的设备称为终端,对应标准输入和标准(错误)输出文件;像磁盘、光盘等外存上的文件则是普通文件
我们在编程时常用的scanf、printf是高级语言提供的标准I/O库函数。我们通过这些函数提出I/O请求,到设备响应并完成I/O请求,这其中涉及到多层次I/O软件和I/O硬件的协作。
比如用户在程序中使用一次printf过程:
来源:网课计算机系统基础(三)
我们可以看到,当我们在用户进程调用printf时,在链接后便可以转到C语言I/O标准库函数printf去执行字符串输出到屏幕上的功能。而printf()通过一系列函数调用,最终会调用函数write()。然后函数write()再通过int $0x80指令(在Linux中该指令代表系统调用,用它可以实现陷入内核让内核做后续处理的功能)在内核空间中找到write对应的系统调用服务例程sys_write来执行。
write函数就是一个系统调用封装函数,它是OS提供的API函数或系统调用。在UNIX或Linux用户程序中,系统调用函数还有open,read,lseek,stat,close
由此可见,是系统调用被封装成用户程序能直接调用的函数。scanf、printf等函数的实现离不开系统调用封装函数。
其关系为:
I/O之间关系

文件

  1. 在进一步介绍系统调用封装函数之前,先提一下文件的概念。在Linux(UNIX)中,所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应的文件的读和写来执行。一句话概括就是,在Linux(UNIX)中,一切皆文件。
  2. 一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。内核返回一个小的非负整数,叫做描述符(fd)。在后续操作中,内核记录有关这个打开文件的所有信息,应用程序只需记住这个描述符。因此描述符对文件的识别非常重要,它是进程文件表项的下标。
  3. Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0,在头文件<unistd.h>中宏定义了STDIN_FILENO表示0),即键盘。标准输出(描述符为1,在头文件<unistd.h>中宏定义了STDOUT_FILENO表示1),即显示器。标准错误(描述符为2,在头文件<unistd.h>中宏定义了STDERR_FILENO表示2)

open和close 打开和关闭文件

进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcnt1.h>

int open(char *filename,int flags,mode_t mode);

open函数将filename转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件:

  • O_RDONLY:只读。
  • O_WRONLY:只写。
  • O_RDWR:可读可写。

flags参数也可以是一个或者更多位掩码的或,为写提供给一些额外的指示:

  • O_CREAT:如果文件不存在,就创建它的一个截断的(空)文件。
  • O_TRUNC:如果文件已经存在,就截断它。如果文件存在,并且是一个普通文件,而且打开方式是O_WRONLY、O_RDWR,则O_TRUNC会清空文件的内容。O_TRUNC|O_RDONLY的话O_TRUNC不会起作用!
  • O_APPEND:在每次写操作前,设置文件位置到文件的结尾处。
  • O_EXCL:该标志一般和O_CREAT配合使用,用来测试文件是否存在,如果指定O_CREAT|O_EXCL,如果文件存在,则open会失败。
  • O_NONBLOCK:非阻塞方式打开文件。
    非阻塞:如果文件没有内容,read直接报错,如果文件没有空间,write直接报错。
    阻塞:如果文件没有内容,read会阻塞(等待直到有数据)。如果文件没有空间,write会阻塞(等待直到有空间)。

mode参数指定了新文件的访问权限位。在应用时可让测试文件的模式位mode与以下掩码做与运算&即可判断权限。
并且有以下宏定义:

#define DEF_MODE S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
#define DEF_UMASK S_IWGRP|S_IWOTH

访问权限位
综上,创建文件的标准写法是:fd=open("filename.txt",O_CREAT|O_TRUNC|O_WRONLY,DEF_MODE);
最后,进程通过调用close函数关闭一个打开的文件。

#include <unistd.h>

int close(int fd);

关闭一个已关闭的描述符会出错。
解释了一些概念后,让我们来看一些程序:
cpstdin.c代码内容:

/* $begin cpstdin */
#include "csapp.h"

int main(void) 
{
    char c;

    while(Read(STDIN_FILENO, &c, 1) != 0) 
	Write(STDOUT_FILENO, &c, 1);
    exit(0);
}

注:#include "csapp.h"中csapp.h头文件包含了所有深入理解计算机系统这本书用到的头文件,在博客最后会给出。程序中Read和Write是对read和write的封装,其内容包含在csapp.h头文件里,针对read和write调用时可能的错误进行了相应输出处理,其它内容还是read和write里的内容没做改变。
我们在命令行上输入gcc cpstdin.c csapp.h csapp.c -lpthread -o cpstdin指令,即可链接相关头文件,实现程序的编译,关于-lpthread可参考该链接
该程序实现的是getchar()函数和putchar()函数的功能,使用read和write调用一次一个字节地从标准输入复制到标准输出,即我们在键盘上输入一串字符串,在回车后该字符串即会在显示器上输出。

username@username-virtual-machine:/mnt/hgfs/chap10_code$ gcc cpstdin.c csapp.h csapp.c -lpthread -o cpstdin
username@username-virtual-machine:/mnt/hgfs/chap10_code$ ./cpstdin
a
a
abc
abc

read和write 读和写文件

因此,我们可以发现,应用程序是通过分别调用read和write函数来执行输入和输出的。

#include <unistd.h>
ssize_t read(int fd,void *buf,size_t n);
//size_t被定义为unsigned long,而ssize_t被定义为long(有符号的大小)
ssize_t write(int fd,const void *buf,size_t n);

注:read和write是不带缓冲的读写:直接从(向)磁盘读(写),没有缓冲。
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。 返回值-1表示一个错误,同时errno被设置。而返回值0表示EOF,即读到文件尾。否则,返回值表示的是实际传送的字节数量。
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。 若成功则返回写的字节数,若出错则为-1。

这里从描述符为STDIN_FILENO(0),即键盘这个文件中读出1个字节存储在变量c的位置,并将c这个位置的这1个字节复制到STDOUT_FILENO(1),即显示器中去。当读出一个字符后,在所读文件中的光标后移一位,可以再读同行的下一个字符;并且写完一个字符后,在所写文件中的光标后移一位,可以再在已写字符的下一个位置继续写。(这是因为字符终端原理机制为行模式,这也是为什么转义字符\n能清空缓冲区输出的原因)
因此在实现时,read函数对用户在键盘输入的字符一次一个地读,直到读完((Read(STDIN_FILENO, &c, 1)返回0表示读到文件末尾)退出循环。write函数也对所读字符一次一个地写在屏幕上,最终呈现出上面的结果。
statcheck.c代码内容:

/* $begin statcheck */
#include "csapp.h"

int main (int argc, char **argv) 
{
    struct stat stat;
    char *type, *readok;

    /* $end statcheck */
    if (argc != 2) {
	fprintf(stderr, "usage: %s <filename>\n", argv[0]);
	exit(0);
    }
    /* $begin statcheck */
    Stat(argv[1], &stat);
    if (S_ISREG(stat.st_mode))     /* Determine file type */
	type = "regular";
    else if (S_ISDIR(stat.st_mode))
	type = "directory";
    else 
	type = "other";
    if ((stat.st_mode & S_IRUSR)) /* Check read access */
	readok = "yes";
    else
	readok = "no";

    printf("type: %s, read: %s\n", type, readok);
    exit(0);
}

该程序应用stat,实现的是查询和处理一个文件的st_mode位(某文件的模式信息,比如它是什么类型文件,是否可读可写)最后在屏幕上输出它的文件类型和访问权限
每个Linux文件都有一个类型(type) 来表明它在系统中的角色:

  • 普通文件(regular file) 包含任意数据。应用程序常常要区分文本文件(text file)二进制文件(binary file),文本文件是只含有ASCII或Unicode字符的普通文件;二进制文件是所有其他的文件。比如,.c、.cpp、.txt均属于文本文件,而.o、.exe属于二进制文件
  • 目录(directory) 是相关联的文件组(即文件夹),其中每个链接都将一个文件名(filename)映射到一个文件,这个文件可能是另一个目录。
  • 套接字(socket) 是用来与另一个进程进行跨网络通信的文件。
  • 其他文件类型包含命名通道、符号链接,以及字符和块设备

stat 读取文件元数据

应用程序能够通过调用stat和fstat函数,检索到关于文件的信息(有时也称为文件的元数据

#include <unistd.h>
#include <sys/stat.h>

int stat(const char *filename,struct stat *buf);
int fstat(int fd,struct stat *buf);
  • filename:要获取状态信息的文件名(带路径)。
  • buf:指向的结构体用来保存文件的状态信息。

fstat函数是相似的,只不过是以文件描述符而不是文件名作为输入。
struct stat结构体记录文件相关所有信息,其内容为:

struct stat  
{   
    dev_t       st_dev;     /* Device鼠标、U盘等设备*/ 
    ino_t       st_ino;     /* inode文件的编号*/    
    mode_t      st_mode;    /* Protection and file type文件类型及权限,是我们主要研究对象*/    
    nlink_t     st_nlink;   /* Number of hard links */    
    uid_t       st_uid;     /* User ID of owner */    
    gid_t       st_gid;     /* Group ID of owner */    
    dev_t       st_rdev;    /* Device type (if inode device) */    
    off_t       st_size;    /* Total size, in bytes */    
    blksize_t   st_blksize; /* Block size for filesystem I/O */    
    blkcnt_t    st_blocks;  /* Number of blocks allocated */    
    time_t      st_atime;   /* Time of last access */    
    time_t      st_mtime;   /* Time of last modification */    
    time_t      st_ctime;   /* Time of last status change */    
};  

st_size成员包含了文件的字节数大小。st_mode成员则编码了文件访问许可位和文件类型。
Linux在sys/stat.h中定义了宏谓词来确定st_mode成员的文件类型。

  • S_ISREG(m) 让文件的st_mode信息与普通文件掩码0100000做与运算来判断该文件是否为普通文件,是则返回真。
  • S_ISDIR(m) 让文件的st_mode信息与目录文件掩码0040000做与运算来判断该文件是否为目录文件,是则返回真。
  • S_ISSOCK(m) 让文件的st_mode信息与套接字文件掩码0140000做与运算来判断该文件是否为套接字文件,是则返回真。

我们再来看statcheck.c程序。让我们事先在statcheck.c同目录下准备一个txt文件abcde.txt和一个新建文件夹test,在命令行上输入gcc statcheck.c csapp.h csapp.c -lpthread -o statcheck指令,运行时在./statcheck后加上相应文件名,得:

username@username-virtual-machine:/mnt/hgfs/chap10_code$ gcc statcheck.c csapp.h csapp.c -lpthread -o statcheck
username@username-virtual-machine:/mnt/hgfs/chap10_code$ ./statcheck abcde.txt
type: regular, read: yes
username@username-virtual-machine:/mnt/hgfs/chap10_code$ 
username@username-virtual-machine:/mnt/hgfs/chap10_code$ ./statcheck test
type: directory, read: yes
username@username-virtual-machine:/mnt/hgfs/chap10_code$ 

总之,该程序首先得到./statcheck后的文件名,如果没带上文件名一开始就会输出错误信息并结束运行。然后调用stat函数,由文件名将该文件的信息赋给结构体变量stat。然后调用宏谓词S_ISREG(m)和S_ISDIR(m)判断它是否为普通文件或目录文件,若都不是则标明为其他文件。然后stat的st_mode跟掩码S_IRUSR做与运算来判断是否可读,最后将记录的这些文件信息输出。

关于缓冲

在前面提到过,read和write是不带缓冲的读写:直接从(向)磁盘读(写),没有缓冲。
而标准I/O库与系统调用封装函数不同的是,标准I/O函数是带缓冲的读写。它将一个打开的文件模型化为一个流。一个流就是一个指向FILE类型的结构的指针。每个ANSI C程序开始时都有三个打开的流stdin、stdout和stderr,分别对应于标准输入、标准输出和标准错误:

#include <stdio.h>
extern FILE *stdin;	//标准输入(描述符为0)
extern FILE *stdout;	//标准输出(描述符为1)
extern FILE *stderr;	//标准错误(描述符为2)

类型为FILE的流是对文件描述符和流缓冲区的抽象。
而流缓冲区的目的是:使开销较高的Linux I/O系统调用的数量尽可能得小。
现在让我们来看一个小程序:
hello.c代码内容:

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

int main()
{
    printf("h");
    printf("e");
    printf("l");
    printf("l");
    printf("o");
    printf("\n");
    fflush(stdout);
    exit(0);
}

之前说过,字符终端原理机制为行模式,因此\n有清空缓冲区的功能。即在语句

	printf("h");
    printf("e");
    printf("l");
    printf("l");
    printf("o");

之后,字符并未被马上输出。而遇到\n才会输出。该程序中
printf("\n"); fflush(stdout); exit(0);这三个语句都有清空缓冲区的功能

运行结果:

username@username-virtual-machine:/mnt/hgfs/chap10_code$ gcc -o hello hello.c
username@username-virtual-machine:/mnt/hgfs/chap10_code$ ./hello
hello
username@username-virtual-machine:/mnt/hgfs/chap10_code$ 

共享文件

可以用许多不同的方式来共享Linux文件。而内核用三个相关的数据结构来表示打开的文件:

  • 描述符表每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
  • 文件表。打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成包括当前的文件位置、引用计数refcnt(即当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为零。
  • v-node表。同文件表一样,所有的进程共享这张v-node表。每个表项包含stat结构中的大多数信息,包括st_mode和st_size成员。

、比如令描述符1和4引用不同的文件,它们会通过不同的打开文件表。这种情况没有共享。
引用不同的文件
比如使用open打开file1.txt和file2.txt,假设它们的文件描述符为3和4(0、1、2是标准输入、标准输出、标准错误,这不会改变),那么描述符表中表项fd3和fd4各指向一个文件表,且每个文件表都指向一个v-node表,这两个v-node表存储了file1.txt和file2.txt的文件信息,是上面提到的struct stat里的内容。

、多个描述符也可以通过不同的文件表表项来引用同一个文件。例如,两次调用open函数来打开同一个文件。这时引用文件的每个描述符表项仍指向不同的文件表,但每个文件表只指向一个v-node表,该表存储了引用的同一文件的信息。关键思想是每个描述符都有它自己的文件位置,因为文件表不同,所以对不同描述符的读操作可以从文件的不同位置获取数据。也就是说,如果两个描述符都去读read同一文件的字符,所读字符后的文件位置是互不影响的。
文件共享
、由前面学的fork,我们可以知道子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件表集合,因此共享相同的文件位置,即父子进程有不同的描述符表,但有相同的表项指向同样的文件表。一个很重要的结果就是,在内核删除相应文件表表项之前,父子进程必须都关闭了他们的描述符。
父子进程
下面我们来看一组代码:
ffiles2.c代码内容:

#include "csapp.h"

int main(int argc, char *argv[])
{
    int fd1;
    int s = getpid() & 0x1;
    char c1, c2;
    char *fname = argv[1];
    fd1 = Open(fname, O_RDONLY, 0);
    Read(fd1, &c1, 1);
    if (fork()) {
	/* Parent */
	sleep(s);
	Read(fd1, &c2, 1);
	printf("Parent: c1 = %c, c2 = %c\n", c1, c2);
    } else {
	/* Child */
	sleep(1-s);
	Read(fd1, &c2, 1);
	printf("Child: c1 = %c, c2 = %c\n", c1, c2);
    }
    return 0;
}

首先准备一个文件abcde.txt,其内容为abcde。
输入gcc ffiles2.c csapp.h csapp.c -lpthread -o ffiles2编译且带上abcde.txt文件./后运行两次的结果:

username@username-virtual-machine:/mnt/hgfs/chap10_code$ gcc ffiles2.c csapp.h csapp.c -lpthread -o ffiles2
username@username-virtual-machine:/mnt/hgfs/chap10_code$ ./ffiles2 abcde.txt
Child: c1 = a, c2 = b
Parent: c1 = a, c2 = c
username@username-virtual-machine:/mnt/hgfs/chap10_code$ ./ffiles2 abcde.txt
Parent: c1 = a, c2 = b
username@username-virtual-machine:/mnt/hgfs/chap10_code$ Child: c1 = a, c2 = c

该程序体现了子进程如何继承父进程的打开文件,展示了它们共享相同的文件位置。

  1. 该程序首先定义若干变量,其中int s = getpid() & 0x1;是让当前父进程的PID与0x1做与运算,由于0x1前面都是0,因此以二进制显示的父进程的PID除最后一位均变为0,最后一位保留自身(因为与1做与运算还是它自身),即若原来父进程PID为偶数(最后一位二进制位为0)则给s赋0,若原来父进程PID为奇数(最后一位二进制位为1)则给s赋1然后读取文件名fd1 = Open(fname, O_RDONLY, 0); Read(fd1, &c1, 1);令其以只读方式打开,并将读出的第一个字符a赋给c1,随后光标后移一位。
    接下来fork创建子进程,fork返回值>0为父进程,执行if,返回值=0为子进程,执行else。
  2. 接下来遇到睡眠,若s为1(父进程PID为奇数),则父进程睡眠滞后执行,子进程sleep(0)无影响而先执行,这是第一种运行情况。子进程先往后读出一个字符b并赋给c2且光标移动并输出Child: c1 = a, c2 = b。接下来父进程执行,由于父子进程共享相同的文件位置,因此父进程会在子进程读完第二个基础上读第三个字符c赋给c2(注意每读完一个字符文件的光标都后移一位表示当前文件内容位置的改变),因此会输出Parent: c1 = a, c2 = c
  3. 同样的,若s为0(父进程PID为偶数),则子进程睡眠滞后执行,父进程sleep(0)无影响而先执行,这是第二种运行情况。那么这时父进程读出第二个字符b,子进程读出第二个字符c。

重定向

Linux shell提供了I/O重定向操作符>,允许用户将磁盘文件和标准输入输出联系起来。例如,键入linux> ls > foo.txt使得shell加载和执行ls程序,将标准输出重定向到磁盘文件foo.txt,即将ls的执行结果输出到指定文件foo.txt上。
此外,dup2函数也可实现I/O重定向:

#include <unistd.h>

int dup2(int oldfd,int newfd);

dup2函数复制描述符表表项oldfd到描述符表项newfd,覆盖描述符表项newfd以前的内容。如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd。
总之,就是让第二个参数对应文件重定向到第一个参数对应文件,放弃之前第二个参数对应文件内容。
例如在共享文件第一张图的基础上调用dup2(4,1),两个描述符现在都指向文件B;文件A已经被关闭了,并且它的文件表和v-node表表项也已经被删除了。从此以后,任何写到标准输出的数据都被重定向到文件B。
重定向
我们来看一个例子:
ffiles1.c代码内容:

#include "csapp.h"

int main(int argc, char *argv[])
{
    int fd1, fd2, fd3;
    char c1, c2, c3;
    char *fname = argv[1];
    fd1 = Open(fname, O_RDONLY, 0);
    fd2 = Open(fname, O_RDONLY, 0);
    fd3 = Open(fname, O_RDONLY, 0);
    dup2(fd2, fd3);

    Read(fd1, &c1, 1);
    Read(fd2, &c2, 1);
    Read(fd3, &c3, 1);
    printf("c1 = %c, c2 = %c, c3 = %c\n", c1, c2, c3);

    Close(fd1);
    Close(fd2);
    Close(fd3);
    return 0;
}

首先准备一个文件abcde.txt,其内容为abcde。
输入gcc ffiles1.c csapp.h csapp.c -lpthread -o ffiles1编译且带上abcde.txt文件./后运行的结果:

username@username-virtual-machine:/mnt/hgfs/chap10_code$ gcc ffiles1.c csapp.h csapp.c -lpthread -o ffiles1
username@username-virtual-machine:/mnt/hgfs/chap10_code$ ./ffiles1 abcde.txt
c1 = a, c2 = a, c3 = b
username@username-virtual-machine:/mnt/hgfs/chap10_code$ 

该程序体现了打开相同文件和重定向时读数据对文件内容位置的改变。
首先定义三个描述符,且都令它们打开相同文件abcde.txt,此时对应上面共享文件二的情况,三个描述符都有自己的文件位置互不干扰
接下来 dup2(fd2, fd3);,即fd2覆盖fd3的原有内容,fd3和fd2指向相同的文件表。
然后三个描述符分别读出一个字符给c1,c2,c3。

  1. 首先fd1读出文件第一个字符a,然后它的文件表光标后移一位。
  2. 接下来fd2读文件一个字符。由于fd1和fd2都有自己的文件位置互不干扰,因此fd2仍是读出第一个字符a,然后它的文件表光标后移一位。
  3. 最后fd3去读,由于fd3已被重定向到fd2文件表位置,因此它会接着fd2读完后的位置继续读一个字符,读出字符b,然后它和fd2共同的文件表光标后移一位。最后的输出我们想要的结果,最后关闭文件。

总结

前面学习了很多知识,现在我们把它们汇总起来看一段代码:
ffiles3.c代码内容:

#include "csapp.h"

int main(int argc, char *argv[])
{
    int fd1, fd2, fd3;
    char *fname = argv[1];
    fd1 = Open(fname, O_CREAT|O_TRUNC|O_RDWR, S_IRUSR|S_IWUSR);
    Write(fd1, "pqrs", 4);	

    fd3 = Open(fname, O_APPEND|O_WRONLY, 0);
    Write(fd3, "jklmn", 5);
    fd2 = dup(fd1);  /* Allocates new descriptor */
    Write(fd2, "wxyz", 4);
    Write(fd3, "ef", 2);

    Close(fd1);
    Close(fd2);
    Close(fd3);
    return 0;
}

/*abcde.txt
pqrswxyzef
*/

首先准备一个文件abcde.txt,其内容为abcde。
输入gcc ffiles3.c csapp.h csapp.c -lpthread -o ffiles3编译且带上abcde.txt文件./后运行的结果:

username@username-virtual-machine:/mnt/hgfs/chap10_code$ gcc ffiles3.c csapp.h csapp.c -lpthread -o ffiles3
username@username-virtual-machine:/mnt/hgfs/chap10_code$ ./ffiles3 abcde.txt
username@username-virtual-machine:/mnt/hgfs/chap10_code$ 

该程序没有输出在屏幕上的信息,但我们发现abcde.txt的原来内容abcde被替换成了pqrswxyznef
该程序运用了之前学到的open函数的flag参数和mode参数的不同掩码,也涉及到了重定向。
首先补充说明以下dup,其基本功能与dup2相似

#include <unistd.h>

int dup(int oldfd);

返回值:如成功则返回新的文件描述符,否则出错返回-1。
函数dup允许你复制一个oldfd文件描述符。存入一个已存在的文件描述符,它就会返回一个与该描述符“相同”的新的文件描述符。即这两个描述符共享相同读写位置和各项权限或flags等等。(来源:重定向编程 dup和dup2函数

  1. 该程序首先获取一个文件名,随后fd1打开它,并打算以方式O_CREAT|O_TRUNC|O_RDWR访问文件,即若文件不存在,则创建(当然这里文件是存在的,故不用创建);若文件存在,则清空原有内容abcde;然后以可读可写方式打开文件。而文件的模式位mode设为S_IRUSR|S_IWUSR,其使用者对文件可读可写(若要创建新文件时才起作用)
  2. 随后,将字符pqrs写入fd1打开的文件,即abcde.txt,字符占四个字节,所以接下来对文件内容的操作在字符s的位置后。
  3. 接下来fd3也打开abcde.txt,以方式O_APPEND|O_WRONLY访问文件,即每次写操作前,设置文件位置到文件末尾处,并且以只写方式打开文件。由于文件已存在,第三个参数没有作用,可设为0.
  4. 写操作将字符jklmn写入fd3指向文件,即abcde.txt。由于打开方式是O_APPEND,因此这占5个字节的字符jklmn一定写在abcde.txt内容的末尾。此时abcde.txt内容为pqrsjklmn。
  5. 接下来重定向,由与dup函数,fd2被重定向到与fd1指向相同文件表,也就是说,fd2也代表文件abcde.txt,并且打开方式与fd1相同,且当前文件位置也与fd1相同。
  6. 将字符wxyz写到fd2所指的位置。注意,fd3对文件的写操作并未改变fd1的位置,因此fd2仍从字符s后的位置开始写,因此覆盖了fd3写的内容,并且写后文件位置在字符z之后。此时abcde.txt内容为pqrswxyzn。
  7. 最后仍是对fd3所指文件写操作。由于fd3的打开方式是O_APPEND,因此字符ef写在文件末尾n之后。注意fd3的文件位置与fd1和fd2不同。此时abcde.txt内容为pqrswxyznef。
  8. 最后关闭三个文件。

综上,三个文件fd1,fd2,fd3都是打开abcde.txt。但是只有fd1和fd2的打开方式相同,对文件写操作时文件位置也相互影响。fd3写操作时一定写在文件末尾,其文件位置与fd1和fd2互不影响,fd1和fd2写操作可以覆盖它写的字符。

该博客参考:《深入理解计算机系统》
慕课:计算机系统基础(三):异常、中断和输入/输出
图片来源:深入理解计算机系统——系统级I/O

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值