系统级I/O入门

本文内容是csapp一书第十章的笔记和代码讲解

10.1Unix I/O
首先来讲一讲一句经典的语录:一切皆文件
在 Linux 中,这是一句很经典的话,自然我们所有的I/O设备也都是文件,而所有的输入和输出都被当作对相应的文件的读和写,不得说这是真是很优雅的方式。
而Unix I/O是什么呢?
我们都知道printf和scanf,他们是ANSI C提供的标准I/O库,可以这么说他们是比较高层的I/O函数。而在linux中是通过Unix I/O函数来实现这些高层函数的,其实就是printf等的源码的意思。举个例子,其中有 open() 和 close() 来打开和关闭文件, read() 和 write() 来读写文件,利用 lseek() 来设定读取的偏移量等等。

10.2文件
每个Linux文件都有一个类型来表明它在系统中的角色,主要有:

  • 普通文件(regular file):包含任意数据,如文本文件(只有ASCLL或Unicode字符的普通文件)和二进制文件(除文本文件外所有)
  • 目录(directory):形象的说就是文件夹
  • 套接字:用于跨网络通信文件

下面是linux目录结构图,根目录为:/ 。
在这里插入图片描述
所有用户对应主目录都在home下。

10.3打开和关闭文件

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char *filename, int flags, mode_t mode);

参数解析
返回值:open函数会返回一个文件描述符,是在当前进程中没有打开的最小描述符
filename:一个文件路径,可以是相对路径,也可以是绝对路径
flags:
O_RDONLY:只读
O_WRONLY:只写
O_RDWR:可读可写
O_CREAT: 文件不存在,就创建一个。
O_TRUNC: 如果文件已经存在,就截断它。这里用词是截断而不是清空,后面我们会知道为什么
O_APPEND:每次写操作,都在文件结尾处添加
mode参数
主要是一些用户访问权限信息,可以根据不同的权限信息的“&”和“|”操作,来赋予要给文件不同权限。这里只举一些例子:
S_IRUSR:使用者(拥有者)能够读这个文件
S_IWUSR:使用者(拥有者)能够写这个文件
S_IXUSR:使用者(拥有者)能够执行这个文件

int close(int fd);

参数: fd 需要关闭文件的文件描述符

10.4读和写文件

ssize_t read(int fd,void *buf,size_t n);
ssize_t write(int fd,const void *buf,size_t n);

参数:从描述符为 fd 的当前文件位置复制最多 n 个字节 到 内存位置 buf 返回:若成功则为读的字节数,若为EOF则为0,若出错为-1 。
需要注意的是返回值的类型ssize_t和参数类型size_t的不同,实际上返回值由于需要返回-1导致其范围比size_t小了一倍

下面是一个例程,实现了我们在键盘输入什么就在显示器输出什么。
具体过程为:从标准输入就是键盘当中输入一个字符的时候,就读取一个字符到缓冲区中去,直到读到一个\n就输出所有缓冲区的内容。
这里也体现了一切皆文件的。STDIN_FILENO和STDOUT_FILENO为标准输入流(键盘等)和标准输出流(显示器等)。其中Read(STDIN_FILENO, &c, 1) != 0类似于getchar

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

int main(void) 
{
    char c;

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

结果

zzz@ubuntu:/mnt/hgfs/shared$ ./a.out
abcd
abcd

**10.6读取文件元数据**
什么是元数据?
元数据是用来描述数据的数据,由内核维护,可以通过 stat 和 fstat 函数来访问,
```c
#include <unistd.h>
#include <sys/stat.h>

int stat(const char*filename,struct stat* buf);
int fstat(int fd,struct stat* buf);

stat的数据结构

struct stat
{
    dev_t           st_dev;     // Device
    ino_t           st_ino;     // inode
    mode_t          st_mode;    // Protection & 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
    unsigned long   st_blksize; // Blocksize for filesystem I/O
    unsigned long   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 change
}

这里讲一个成员:st-mode其他的大家可自行谷歌百度
st-mode代表文件类型:
S_ISREG(m)是否为普通文件
S_ISDIR(m)是否为目录文件
S_ISSOCK(m)是否为网络套接字

10.8共享文件 和 10.9重定向
这是一个挺重要的话题,我们首先很容易想到一个文件是可以被打开多次的,这会引发一系列的问题,都需要共享文件这个概念来解答。

首先内核用三个相关的数据结构来表示打开的文件

  • 描述符表 每个进程都有独立的描述符表,表项是由进程打开的文件描述符来索引的。每个描述符表项指向文件表中的一个表项。
  • 文件表 打开文件的集合是由一张文件表来表示的,所有进程共享。它记录了当前文件的位置(指向同一个文件表则共享文件位置),当前指向该表项的描述符表项数(成为引用计数)和一个指向v-node表中对应表项的指针。当引用计数为0是,内核会自动删除这个文件表表项。
  • v-node表。所有进程共享,包含了stat结构中的大多数信息。

这里借用不周山系列几张图来解释这些概念,原文网址:https://wdxtub.com/csapp/thin-csapp-6/2016/04/16/

1.打开两个文件A和B,可以看到fd1指向文件A(之前说过fd1 fd2 fd3是固定的文件描述符,所以这个是标准输出流文件),而fd4应该就是程序员后面自己又open的一个文件了。在这里插入图片描述
2.fork的影响
我之前讲fork的博客讲到过,子进程继承父进程一切,所以描述符表自然也继承了,所以生成了一个和之前描述符表一摸一样的表,注意文件A和B的引用次数+1,这和这篇文章以前讲的 当引用计数为0是,内核会自动删除这个文件表表项是吻合的。
在这里插入图片描述

了解了上面的概念下面介绍一下重定向
顾名思义,重定向就是改变fd指向的文件表,对应第一张图对fd1重定向后:
在这里插入图片描述
重定向使用函数

int dup2(int oldfd, int newfd);

这个函数就是复制oldfd到newfd,特别要注意顺序哦

最后结合两个我们考试题目落到实践
1.

#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;
}

这个很常规,画画图就出来了
输出结果顺序为 a a b

一开始fd1,fd2,fd3拥有不同的打开文件表接着,然后dup2让fd2指向了fd3指向的打开文件表。我之前写概念就特意提及,打开文件表中拥有文件位置信息。所以dup2后fd2和fd3实际上是共享文件位置的,也就是共享我们在window操作系统下看到的光标的位置。剩下的就很好理解了

2

#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;
}

这道题考试时纠结了很久,脑海主要的问题就是对O_TRUNC的争议,还好最后对了:) 5分啊!
我之前讲概念是也特意为这里铺垫了一些,我说这个中文翻译是截断,而不是清空是有他的道理的,我考试时也是受到截断这个词的启发。
废话不多说开始分析:

这里有一个新的函数dup

int dup(int oldfd);

考试的时候其实是猜的,猜是由oldfd复制一个fd,事实上也是这样。

解题过程:再从头开始看,一开始通过fd1写入pqrs,此时fd1指向的打开文件表的文件位置指向第四个字母后面。然后通过fd3最加(通过参数O_APPEND知道是最加)jklmn,记住此时fd3的光标位置。然后通过复制fd1的fd2再写入wxyz,既然是复制,自然也是带O_TRUNC的,所以会有截断发生,wxyz被截断而前面的被保留。
这也是为什么被翻译成截断而不是清空,因为按照正常的情况,我们open一个文件,此时fd一定在文件开头,这时的截断效果看上去和清空一样,但这是很具有误导性的称呼,我们在这道题看到O_TRUNC和清空完全不同,详细地说效果是从光标处开始覆盖后面地内容,没有被覆盖的内容则被清空。啰嗦地讲了这么多,本道题已经迎刃而解了,最后答案是pqrswxyzef。

发布了30 篇原创文章 · 获赞 5 · 访问量 6931

猜你喜欢

转载自blog.csdn.net/weixin_44735312/article/details/103391253