系统级I/O

输入/输出(I/O)是主存和外部设备之间复制数据的过程。

输入:从I/O设备复制数据到主存。
输出:主存复制数据到I/O设备

所有语言的运行时系统都提供执行I/O的较高级别的工具。例如:ANSI C提供标准I/O库,包含像printf和scanf这样执行带缓冲区的I/O函数。

在Linux系统中,是通过使用由内核提供的系统级Unix I/O函数来实现较高级别的I/O函数的。

文件#

在Linux系统中,文件可以看作字节序列,且所有I/O设备都是用文件来表示,甚至连内核都是用文件表示。

因为I/O设备也是文件,所以内核可以利用Unix I/O的简单接口来处理输入输出。

如何区别不同的文件类型呢?

每个Linux文件都有一个类型(type)来表明它在系统中的角色:

  • 普通文件(regular file)
  • 目录(directory)
  • 套接字(socket)
  • 其他文件类型:有名管道(named pipe)、符号链接(symbolic link)、字符和块设备(character and block device)

普通文件#

普通文件包含任意数据。

应用程序:需要区分文本文件(text file)和二进制文件(binary file)

  • 文本文件:只含有ASCII或Unicode字符的普通文件。是一系列的文本行。
  • 二进制文件:除文本文件外全部是二进制文件

内核:文本文件和二进制文件没有区别

目录#

目录是包含一组链接(link)的文件,其中每个link都将一个文件名(filename)映射到一个文件,这个文件可能是一个目录。

每个目录至少包含两个条目:...

Linux内核将所有文件都组织成一个目录层次结构,由名为/的根目录确定。

内核会为每个进程保存当前工作目录(cwd),可以使用cd命令来更改

套接字#

套接字:用来和另一个进程进行跨网络通信的文件。

打开文件#

NAME
       open, openat, creat - open and possibly create a file

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

       int open(const char *pathname, int flags);
       int open(const char *pathname, int flags, mode_t mode);

RETURN VALUE
       open() return the new file descriptor (a nonnegative integer), 
	   or -1 if an error occurred (in which case, errno is set appropriately).

flags参数:进程打算如何访问文件
O_RDONLY: 只读
O_WRONLY: 只写。
O_RDWR: 可读可写。
以上只能出现一个

flags也可以是一个或更多位掩码的或,为写提供一些额外的指示
O_CREAT: 如果文件不存 ,就创建它的一个截断的 (truncated) (空)文件
O_TRUNC: 如果文件巳经存在,就截断它
O_APPEND: 在每次写操作前,设置文件位置到文件的结尾处

若有O_CREAT参数,需要添加mode参数,指定了新文件的访问权限位。

在Linux系统中,umask用于确定新创建文件和目录权限的设置。每一个进程都有一个umask。当进程创建文件时,会用mode&~umask对文件进行权限设置

返回值:一个最小的整数文件描述符(file descriptor)。错误-1

由于每个Linux Shell创建的时候都会默认打开三个文件(stdin/stdout/stderr)

关闭文件#

open一个文件,都需要使用close来关闭文件

#include <unistd.h> 

int close(int fd);

关闭一个已关闭的描述符会出错。因此,在close文件时,一定要进行检查

读取文件#

打开和关闭文件之间就是读取文件,实际上是将文件中对应的字节复制到内存中,并更新文件指针。

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

成功:返回成功读取的字节数,EOF则为0,nbytes < 0 出错:-1

nbytes < sizeof(buf)这种情况(short counts)发生不会出错。

写入文件#

write函数从内存位置buf复制至多n个字节到fd的当前文件位置。

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

成功:实际写入的字节数。出错-1

前面提到的short count出现的原因有:
在读的时候遇到EOF
从终端中读取文本行
读取和写入网络
也有可能在下面的情况下发生:
从磁盘文件中读取(除EOF外)
写入到磁盘文件

读取文件元信息#

应用程序可以调用statfstat函数,检索到关于关于文件的源数据

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

成功:0;
出错:-1;

fstat函数以一个文件描述符作为输入,并填写下面的一个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_ino: 磁盘号
st_mode: 文件的类型,文件权限
st_nlink: 文件的硬链接数
st_size:文件的字节数大小

Linux在sys/stat.h中定义了一系列宏来确定st_mode的文件类型
S_ISREG(mode): 是普通文件吗
S_ISDIR(mode): 是目录文件吗
S_ISSOCK(mode): 是网络套接字吗
S_ISLNK(mode): 是符号链接吗

int main (int argc, char **argv)
{
    struct stat stat;
    char *type, *readok;
    
    Stat(argv[1], &stat);
    if (S_ISREG(stat.st_mode)) // 确定文件类型
        type = "regular";
    else if (S_ISDIR(stat.st_mode))
        type = "directory";
    else
        type = "other";
    
    if ((stat.st_mode & S_IRUSR)) // 检查读权限
        readok = "yes";
    else
        readok = "no";
    
    printf("type: %s, read: %s\n", type, readok);
    exit(0);
}

读取目录内容#

在读取目录内容之前需先试用opendir打开目录。以路径名为输入,返回指向目录流(directory stream)的指针。流是对条目有序列表的抽象,这里是指目录项的列表。

#include <sys/types.h> 
#fnclude <dirent.h> 

	DIR *opendir(const char *name);

返回 若成功 则为处理的指针 ;若出错 ,则 NULL

应用程序可以用readdir函数读取目录内容

SYNOPSIS
       #include <dirent.h>

       struct dirent *readdir(DIR *dirp);

返回:成功,指向下一个目录项的指针
     没有更多的目录项 / 出错,则为NULL
     若出错,会设置errno,因此,可以更具errno来判断是否出错

每个目录项都是一个结构,其形式如下:

struct dirent { 
    ino_t d_ino; 	// 文件位置
    char d_name[256]; 	// 文件名字 
};

虽然有些Linux系统在dirent结构体中包含其他成员,但只有这两个对其他系统来说都是标准的

下面的示例描述如何使用readdir

#include <func.h>

int main(int argc, char* argv[])
{
    // 1. 打开目录流
    DIR *pdir = opendir(argv[1]);
    if (pdir == NULL) {
        error(1, errno, "opendir");
    }

    // 2. 遍历目录流
    errno = 0;
    struct dirent *curr;
    while ((curr = readdir(pdir)) != NULL){
        // 打印目录信息
        printf("d_ino = %lu, d_name = %s", curr->d_ino, curr->d_name);
    } // curr == NULL
    if (errno) {
        error(1, errno, "readdir");
    } 

    // 3. 关闭目录流
    closedir(pdir);
    return 0;
}

共享文件#

可以用很多方式来共享Linux文件。那么Linux内核是如何表示打开文件的呢?

实际上,Linux内核使用了三个相关的数据结构来表示打开的文件:

  • 文件描述符表(file descriptor table)。每个进程都有自己独立的描述符表,它的表项由进程打开的文件描述符来索引。每个文件描述符指向文件表中的一个表项。

  • 文件表(file table)。打开文件的集合是由一张文件表来表示,所有进程共享这张文件表

    • 文件表的表项:
      • 当前文件的位置(current file offset)、
      • 引用计数(reference count)、
      • 以及一个指向v-node表中对应表项的指针(vnode pointer)。
  • vnode表(vnode table)。所有进程共享。在该表中包含stat结构体中的大多数信息,

img

下图是一个典型的情况,没有共享文件,并且每个文件描述符对应不同的文件

img

多个文件描述符指向同一个文件

img

父子进程是如何共享文件的呢?

假设在fork之前,父进程有图10-12所示的打开文件。图10-14展示了调用fork后的情况。子进程有父进程文件描述符表的副本。父子进程共享相同的打开文件表集合,因此,共享相同的打开文件。

img
假设磁盘文件foobar.txt由6个ASCII码字符组成“foobar”
    
int main() 
{ 
    int fd1, fd2; 
    char c; 
    fd1 = Open("foobar.txt", O_RDONLY, O); 
    fd2 = Open("foobar.txt", O_RDONLY, 0); 
    Read(fd1, &c, 1); 
    Read(fd2, &c, 1); 
    printf("c = %c\n", c); 
    exit(O); 
}

打印:c = f

为什么会打印c = f呢,fd1和fd2都有自己的打开文件表,,所以每个描述符对千 fo bar.txt 都有它自己的文件位置 。因此,fd2的读操作会读取foobar.txt的第一个字节

int main() 
{ 
	int fd; 
	char c; 

	fd = Open("foobar.txt", O_RDONLY, O); 
	if (Fork() == 0) { 
		Read(fd, &c, 1); 
		exit(O); 
	} 
	Wait(NULL); 
	Read(fd, &c, 1); 
	printf("c = %c\n", c); 
	exit(O); 
}
打印:c = o

I/O重定向#

Linux shell提供了I/O重定向操作符。允许用户将磁盘文件和标准输入输出联系起来。

Linux:ls > foo.txt

使得 shell 加载和执行 ls 程序,将标准输出重定向到磁盘文件 foo.txt。

那么I/O重定向是如何工作的呢?一种方式是使用dup2函数

#include <unistd.h> 
	int dup2(int oldfd, int newfd);

返回:成功,为非负的描述符
     -1 出错

dup2函数复制描述符表项oldfdnewfd,覆盖描述符表项newfd之前的内容。如果newfd已经打开,dup2会在复制oldfd之前关闭newfd

假设在调用dup(4,1)之前,状态如图10-12所示。

其中fd1(标准输出)对应文件A(比如一个终端),
fd4对应文件B(比如一个磁盘文件)。A和B的引用计数都等于1。

图10-15显示调用dup2(4,1)之后的情况。
两个描述符都指向文件B;
文件A被关闭,且它的文件表和vnode表项都已经被删除
文件b的引用计数增加。

img
int main(argc, argv[])
{ 
	int fd1, fd2; 
	char c; 

 	fd1 = Open("foobar.txt", O_RDONLY, O); 
	fd2 = Open("foobar.txt", O_RDONLY, O); 
	Read(fd2, &c, 1); 
	Dup2(fd2, fdl); 
	Read(fd1, &c, 1); 
	printf("c = %c\n", c); 
	exit(O); 
}

打印 c = o

这是因为在第一次read后会修改文件打开表中当前文件的位置。重定向会指向相同的文件打开表。因此会输出c

标准I/O#

C语言定义了一组高级输入输出函数,称为标准I/O库,为程序员提供 Unix I/O的较高级别的替代。

在这个库中,提供了一系列函数操作文件

  • 打开和关闭文件: fopen, fclose
  • 读取和写入字节: fread, fwrite
  • 读取和写入行: fgets, fputs
  • 格式化读取和写入: fscanf, fprintf

标准I/O库将一个打开的文件模型化为一个流。对程序员来说,一个流就是一个指向FILE类型的指针。FILE类型的流实际上是对文件描述符和缓冲区在内存中的抽象。每个C程序开始时都有三个打开的流 stdin stdout、stderr

#include <stdio.h>
extern FILE *stdin;     // 标准输入 descriptor 0
extern FILE *stdout;    // 标准输出 descriptor 1
extern FILE *stderr;    // 标准错误 descriptor 2

int main()
{
    fprintf(stdout, "Hello, Da Wang\n");
}

为什么需要缓冲区呢?

其目的是使开销较高的Linux I/O系统调用的数量尽可能得小。

程序经常会一次读入或者写入一个字符,比如 getc, putc,也可能会一次读入或者写入一行,比如 gets, fgets。如果用 Unix I/O 的方式来进行调用,是非常昂贵的,比如说 每次使用readwrite 一个字符的读或写,因为需要内核调用,代价相当的昂贵。

解决的办法就是利用 read 函数一次读取一块数据,然后再由高层的接口,一次从缓冲区读取一个字符(当缓冲区用完的时候需要重新填充)

标准 C I/O 提供了带缓存访问文件的方法,使用的时候几乎不用考虑太多,但是如果要获取文件元信息,就需使用 Unix I/O 中的 stat 函数。

I/O函数的正确使用(待补充)#

img

只要有可能就使用标准I/O。对磁盘和终端设备I/O来说,标准I/O函数是首选方法。

对网络套接字设备的I/O使用Unix I/O。

作者:notob

出处:https://www.cnblogs.com/notob/p/18125925

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   fowind  阅读(45)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示