关于I/O的那些事(深入理解系统级I/O)

本文详细解析了Linux系统级I/O的基本概念,包括文件描述符、文件操作、元数据访问、文件共享机制以及I/O重定向。通过具体的代码示例,展示了如何在Linux环境下进行文件的读写、打开关闭、元数据查询以及描述符复制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Unix/Linux哲学中有这么一句话:一切皆文件。

而文件实际上可以看做是字节的序列。
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做相应文件的读和写来执行,所以内核可以利用称为 Unix I/O 的简单接口来处理输入输出,比如使用 open() 和 close() 来打开和关闭文件,使用 read() 和 write() 来读写文件,或者利用 lseek() 来设定读取的偏移量等等(此段转载至CSAPP——系统级I/O

@通常Linux shell创建的每个进程开始时都会自动打开三个文件:

0:标准输入
1:标准输出
2:标准错误

@文件操作有:open,close,read,write,stat,lseek,dup2

@每个文件在Linux中都有一个类型:普通文件(包含任意数据)、套接字(和另一台机器上的进程通信类型)、目录(相关一组文件的索引)等

接下来从文件操作开始深入理解系统级I/O

打开文件:

进程调用open函数打开一个已存在的文件或创建一个新的文件

open函数打开成功会返回一个文件描述符,出错则返回-1,且文件描述符是在当前进程中没有打开的最小描述符,根据上文所说的,每个进程的开始都会自动打开三个文件,所以用户打开文件的最小进程号是3,下面来看一段代码:(即open函数)

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

int open(char* filename,int flags, mode_t mode);
  • char* filename:文件名
  • int flags:指明进程打算如何访问该文件
  • mode_t mode:指定了新文件的访问权限位

对于flags有如下宏定义:

  • O_RDONLY:只读
  • O_WRONLY: 只写
  • O_RDWR: 可读可写
  • O_CREAT: 文件不存在,就创建一个它的截断的空文件
  • O_TRUNC: 如果文件已经存在,就截断
  • O_APPEND: 在每次写操作前,设置文件位置到文件结尾处

而mode参数也有如下宏定义:

  1. S_IRUSR:使用者(拥有者)能够读这个文件
  2. S_IWUSR:使用者(拥有者)能够写这个文件
  3. S_IXUSR:使用者(拥有者)能够执行这个文件
  4. S_IRGRP:拥有者所在组的成员能够读这个文件
  5. S_IWGRP:拥有者所在组的成员能够写这个文件
  6. S_IXGRP:拥有者所在组的成员能够执行这个文件
  7. S_IROTH:其他人(任何人)能够读这个文件
  8. S_IWOTH:其他人(任何人)能够写这个文件
  9. S_IXOTH:其他人(任何人)能够执行这个文件

关闭文件:

看如下代码:

#include <unistd.h>
int close(int fd);

不能关闭一个已经关闭了的文件,否则会报错(必须仔细检查函数的参数和返回值)

读写文件:

调用read函数和write函数进行输入和输出
上代码:

#include <unistd.h>
ssize_t read(int fd,void *buf,size_t n);
ssize_t write(int fd,const void *buf,size_t n);

read函数的作用是从描述符为fd的文件的当前位置复制最多n个字节到位置buf
而write函数的作用是从内存buf出至多复制n个字节到描述符为fd的当前文件位置

接下来就是正式跑代码的时候了,上代码:

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

int main(void) 
{
    char c;

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

运行结果:
在这里插入图片描述
其实这个程序就是在循环读入,每次从标准输入就是键盘当中输入一个字符的时候,就自动读取一个字符到缓冲区中去,直到读到一个\n(回车)就输出所有缓冲区的内容到标准输出就是屏幕上面。

读取文件元数据:

什么是元数据? 元数据就是用来描述数据的数据,由内核维护,通过stat和fstat函数来访问

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

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

在Linux中可以用 man 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
}

接下来看看另一段小程序:

/* $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);
}


运行结果:
在这里插入图片描述
这个程序主要是用来判断一个文件类型是普通文件,目录还是其他;以及这个文件是否可读。
Linux在sys/stat.h中定义了宏谓词来确定st-mode成员的文件类型:
S_ISREG(m)是否为普通文件
S_ISDIR(m)是否为目录文件
S_ISSOCK(m)是否为网络套接字
所以程序的解释为:
参数"abcde.txt"先传到if的括号中,条件满足,abcde.txt是一个普通的文本文件,判断结束,printf。

共享文件:

Linux文件可以用很多方式进行共享,而内核通常用三个相关的数据结构来表示打开的文件

  1. 描述符表
    每个进程都有独立的描述符表,表项是由进程打开的文件描述符来索引的。每个描述符表项只想文件表中的一个表项。
  2. 文件表
    打开文件的集合是由一张文件表来表示的,所有进程共享。它记录了当前文件的位置,当前指向该表项的描述符表项数(成为引用计数)和一个指向v-node表中对应表项的指针。当引用计数为0是,内核会自动删除这个文件表表项。
  3. v-node表
    所有进程共享,包含了stat结构中的大多数信息,包括st_mode和st_size成员。
    在这里插入图片描述
    打开了两个不同的文件,fd1和fd4通过不同的文件表表项A和B来引用两个不同的文件,这里没有共享文件,并且每个描述符对应一个不同的文件。
    在这里插入图片描述
    两次打开了同一个文件,因为每个描述符都有他自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。
    还有一个比较特殊的情况,就是关于子进程和父进程的问题,如图所示:
    在这里插入图片描述
    看一个小程序:
#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
运行结果:
在这里插入图片描述
从结果中可以看出:
1.先打开了abcde.txt,并且读取了一个字符,现在光标停留在了ab之间
2.进行了fork,子进程复制了父进程的环境。
3.然后两个进程都遇到了sleep,接着先继续执行子进程,它读了一个字符,因为先前光标停留在ab之间,所以此时c2读到的是b
4.child执行完后回到父进程,父进程和子进程的文件表表项相同,所以光标在b后,往后读一个,读到了c
5.最后进行输出得到了如图所示的结果

I/O重定向:

I/O重定向能帮助用户将磁盘文件和标准输入输出联系起来
接下来用函数来理解:

#include <unistd.h>
int dup2(int oldfd,int newfd);
int dup(int oldfd);
  • dup2函数:用oldfd的文件表表项替换掉newfd的文件表表项,此外如果newfd是打开的状态的话,会需要先关闭掉newfd。
  • dup函数:直接再建一个文件,文件的文件项就是oldfd

跑一个程序看看:

#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文件内容为pqrswxyzef
运行结果:
在这里插入图片描述
1.打开文件,返回fd1,写入“pqrs”,此时光标在文件末尾
2.fd3打开这个文件,O_APPEND表明光标停留在文本文件的最后一个字符后面,并写入"jklmn"
3.dup函数,将文件描述符表fd1文件指向了fd2,简单理解就是fd2和fd1是一样的,所以可知fd2光标在pqrs后面
4.写fd2,此时写的"wxyz"会覆盖"jklm"
5.写fd3,因为fd3的光标在最末尾,所以ef写在了最后

本文参考链接:
1.https://blog.youkuaiyun.com/qq_44871442/article/details/103375768#_12
2.《计算机系统基础》第十章——系统级I/O

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值