跳转至

4.2 I/O

Wed Lecture

At the lowest level, an operating system will only communicate using bytes, not with higher-level integers or floating-point values. C11 employs arrays of characters to hold the bytes in requests for raw input and output.

在请求原始输入和输出时,C11 会使用字符数组来保存这些请求中的字节。操作系统只认字节,而字符数组正是将数据打包成字节的形式,因此字符数组在这里起到关键作用。

File Descriptors

在基于 Unix 的操作系统中,文件描述符是用于标识各种通信渠道的整数值。这些渠道可以是文件、进程间通信管道、设备,或者网络连接等。文件描述符作为简单的整数值,提供了与这些资源的接口。

在 C11 编程中,文件描述符和字符数组经常组合使用来向操作系统发出输入输出请求。以文件读取为例,程序通过整数文件描述符告诉操作系统要访问哪个文件,并将读取的内容存入字符数组中,系统 read 细节可以参考 man 2 open

以下为读取文件全部内容完整代码:

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

#define  MYSIZE      10000

void read_using_descriptor(char filename[])        
{
    // 尝试以只读方式打开文件
    int fd = open(filename, O_RDONLY);

    // 检查文件是否成功打开
    if(fd == -1) {
        printf("cannot open '%s'\n", filename);
        exit(EXIT_FAILURE);
    }

    // 定义字符数组用于保存文件内容
    char buffer[MYSIZE];
    size_t got;

    // 多次读取文件直到文件末尾
    while((got = read(fd, buffer, sizeof buffer)) > 0) {
        write(STDOUT_FILENO, buffer, got);
    }
    // 当 read 函数返回 0 时,表示已经到达文件末尾(EOF),没有更多的数据可以读取。这时循环条件 got > 0 不再满足,循环退出。
      // 如果 read 函数返回负值,则表示发生了错误,循环也会退出。

    // 关闭文件,表示不再访问
    close(fd);
}

int main() {
    read_using_descriptor("haversine.c");
}

其中 fcntl.h 为底层提供的系统调用,并不是 C11 的标准库函数,提供了 open, read, close 等负责与操作系统交互并执行低层次的输入输出操作。unistd.h提供标准文件卸载函数的 close 掉调用通道后释放内存

int fd = open(filename, O_RDONLY) open and read only 的缩写只读;其次打开文件可能会遇到几种情况,文件不存在 ENOENT 文件没有权限 EACCESS 和文件系统达到打开文件数量的上限 EMFILE 或 ENFILE

系统成功打开文件后会返回正整数,一个大于 3 的值,0 1 2 分别属于 stdin, stdout, stderr 的标准文件描述符。当文件打开失败时,open 函数会返回 -1

if(fd == -1) {
    printf("cannot open '%s'\n", filename);
    exit(EXIT_FAILURE);
}

EXIT_FAILURE 是一个宏,定义在 中,它表示程序的异常退出失败状态。使用 EXIT_FAILURE 可以更加明确地告诉操作系统程序由于某种错误而退出。与之相对的宏是 EXIT_SUCCESS,表示程序的正常退出

与 Python 不同的是 C 需要显示手动管理内存,因此底层的缓冲和内存管理需要手动管理,数组 buffer 的作用为临时保存文件内容,提高效率并减少系统调用次数,同时我们也需要手动设置内存大小:

char buffer[1024];
int fd = open("file.txt", O_RDONLY);
int bytes_read = read(fd, buffer, sizeof(buffer));

size_t 类型为无符号整数类型,通常用于描述内存或数据块大小,在 32 位系统上,size_t 通常是 4 字节长(32 位),在 64 位系统上,它通常是 8 字节长(64 位)。因此,它足够大,能够表示系统内存中对象的最大可能大小。

printf("%lu", got); //2185 个读取文件 read 流字节数

USB 和网络连接的情况下 got 这种容易出错

Copy a file using file descriptors

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

#define  MYSIZE      10000

int copy_file(char destination[], char source[])        
{
//  ATTEMPT TO OPEN source FOR READ-ONLY ACCESS
    int fd0    = open(source, O_RDONLY);
//  ENSURE THE FILE COULD BE OPENED
    if(fd0 == -1) {
        return -1;
    }

//  ATTEMPT TO OPEN destination FOR WRITE-ONLY ACCESS
    int fd1    = open(destination, O_WRONLY);
//  ENSURE THE FILE COULD BE OPENED
    if(fd1 == -1) {
        close(fd0);
        return -1;
    }

//  DEFINE A CHARACTER ARRAY TO HOLD THE FILE'S CONTENTS
    char   buffer[MYSIZE];
    size_t got;

//  PERFORM MULTIPLE READs OF FILE UNTIL END-OF-FILE REACHED  
    while((got = read(fd0, buffer, sizeof buffer)) > 0) {  
        if(write(fd1, buffer, got)) != got) {  
            close(fd0); close(fd1);
            return -1;
        }
    }

    close(fd0); close(fd1);
    return 0;
}

和 read 代码前期操作一致,除了双 open 操作在 while 循环处:

这里的 while loop condition 的逻辑是:while 循环会不断地从文件中读取数据并写入标准输出,直到读取到文件末尾(read 返回 0)或发生错误(read 返回负值)。

只要满足这个 condition 就继续运行,不满足直接退出

while((got = read(fd0, buffer, sizeof buffer)) > 0) {  
    if(write(fd1, buffer, got)) != got) {  
        close(fd0); close(fd1);
        return -1;
    }
}

read(fd, buffer, sizeof buffer):从文件描述符 fd 中读取数据,最多读取 sizeof buffer 字节的数据,并将这些数据存储在 buffer 中。这个函数返回实际读取的字节数。write 函数尝试将 buffer 中的 got 字节数据写入到 fd1 指定的文件描述符。它返回实际写入的字节数。

这个条件检查 write 函数的返回值是否等于 got。如果返回值不等于 got,说明写入操作没有完全成功(可能部分数据没有写入,或者发生了错误)。

Read and writing text files

读取 txt 和读取,写入 byte 的方法不一样,因为是读取 txt 一般都是一行一行的读取,不是网络字节。因此用 std 库里的 fopen 和 fclose 函数进行高级操作:

#include <stdio.h>

#define DICTIONARY      "/usr/share/dict/words"

....
//  ATTEMPT TO OPEN THE FILE FOR READ-ACCESS
    FILE   *dict = fopen(DICTIONARY, "r");

//  CHECK IF ANYTHING WENT WRONG
    if(dict == NULL) {
        printf( "cannot open dictionary '%s'\n", DICTIONARY);
        exit(EXIT_FAILURE);
    }

//  READ AND PROCESS THE CONTENTS OF THE FILE
    ....

//  WHEN WE'RE FINISHED, CLOSE THE FILE
    fclose(dict);

操作参数与 Python 一致

r, r+, w, w+, a, a+

FILE   *dict = fopen(DICTIONARY, "r");

用 FILE 类型声明,* 表明 dict 是一个指向 FILE 类型数据的指针

有些代码中会出现 br 之类的用二进制模式打开文件,这种标志只会在 Windows 有效,其他系统会被胡咧,为上个世纪遗留下来的一些老标准

Read txt line by line

#include <stdio.h>

....
    FILE   *dict;
    char   line[BUFSIZ];

    dict = fopen( ..... );
    ....

//  READ EACH LINE FROM THE FILE,
//  CHECKING FOR END-OF-FILE OR AN ERROR
    while( fgets(line, sizeof line, dict) != NULL ) {  
        ....
        ....     // process this line
        ....
    }
//  AT END-OF-FILE (OR AN ERROR), CLOSE THE FILE
    fclose(dict);

从检查整体到一行行检查:

结果发现读取的每一行实际上结尾都会有 \n\0, 在 Windows 系统上甚至还多一个回车符:\r\n\0 因此编写一个函数将其替换为 null byte

// REMOVE ANY TRAILING end-of-line CHARACTERS FROM THE LINE
void trim_line(char line[])
{
    int i = 0;

//  LOOP UNTIL WE REACH THE END OF line
    while(line[i] != '\0') {

//  CHECK FOR CARRIAGE-RETURN OR NEWLINE
        if( line[i] == '\r' || line[i] == '\n' ) {
            line[i] = '\0'; // overwrite with null-byte
            break;          // leave the loop early
        }
        i = i+1;            // iterate through character array
    }
}

但是这样的操作是对 variable 的副本进行编辑

Writing text output to a file

fgets() 从文件中获取一行文本,用 fputs 写入一行文本

fputs 的文件指针必须提前已经 open for writing 或 appending

Copy a text file using file pointers

以下就是带有全部完整正常功能的全部程序:

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

void copy_text_file(char destination[], char source[])
{
    FILE        *fp_in   = fopen(source, "r");
    FILE        *fp_out  = fopen(destination,  "w");

//  ENSURE THAT OPENING BOTH FILES HAS BEEN SUCCESSFUL
    if(fp_in != NULL && fp_out != NULL) {
        char    line[BUFSIZ];

        while( fgets(line, sizeof line, fp_in) != NULL) {  
            if(fputs(line, fp_out) == EOF) {
                printf("error copying file\n");
                exit(EXIT_FAILURE);
            }
        }
    }
//  ENSURE THAT WE ONLY CLOSE FILES THAT ARE OPEN
    if(fp_in != NULL) {
        fclose(fp_in);
    }
    if(fp_out != NULL) {
        fclose(fp_out);
    }
}

由于 fputs 有 return 操作,因此在 if 过程中就已经写入进 out 指针

Reading and writing files of binary data

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

void copyfile(char destination[], char source[])
{
    FILE        *fp_in   = fopen(source, "rb");
    FILE        *fp_out  = fopen(destination,  "wb");

//  ENSURE THAT OPENING BOTH FILES HAS BEEN SUCCESSFUL
    if(fp_in != NULL && fp_out != NULL) {

        char    buffer[BUFSIZ];
        size_t  got, wrote;

        while( (got = fread(buffer, 1, sizeof buffer, fp_in)) > 0) {  
            wrote = fwrite(buffer, 1, got, fp_out);
            if(wrote != got) {
                printf("error copying files\n");
                exit(EXIT_FAILURE);
            }
        }

    }

//  ENSURE THAT WE ONLY CLOSE FILES THAT ARE OPEN
    if(fp_in != NULL) {
        fclose(fp_in);
    }
    if(fp_out != NULL) {
        fclose(fp_out);
    }
}

操作参数后加入 b 然后按照 1 字节 1 字节的大小操作复制,一般用于网络传输和协议解包,让数据项大小相同

size_t  fread(void *ptr, size_t eachsize, size_t nelem, FILE *stream);

Tape

Professor 提出了磁带擦出录制的概念,用到了 rewind 函数覆写:

#include <stdio.h>

int   intarray[ N_ELEMENTS ];
int   got, wrote;

//  OPEN THE BINARY FILE FOR READING AND WRITING
    FILE  *fp = fopen(filename, "rb+");
    ....

    got = fread( intarray, sizeof int, N_ELEMENTS, fp);  
    printf("just read in %i ints\n", got);

//  MODIFY THE BINARY DATA IN THE ARRAY
    ....

//  REWIND THE FILE TO ITS BEGINNING
    rewind(fp);

//  AND NOW OVER-WRITE THE BEGINNING DATA
    wrote = fwrite( intarray, sizeof int, N_ELEMENTS, fp);  
    ....

    fclose(fp);

rewind 函数将给定的文件流 stream 的文件位置指针重置到文件的开头。这意味着任何后续的文件读写操作将从文件的起始位置开始。

Network

在不同的硬件架构下写入和读取出来的二进制数据结果不一样,这个问题在往返不同架构和跨网络的时候需要被解决,详细在计算机网络课程中的数据的终结性

32 奔腾处理器

#include <stdio.h>

#define N     10

int array[N];

for(int n=0 ; n < N ; ++n) {
    array[n]  =  n;
}

fwrite(array, N, sizeof int, fp_out);

32 PowerPC 读取

#include <stdio.h>

#define N     10

int array[N];

fread(array, N, sizeof int, fp_in);  

for(int n=0 ; n < N ; ++n) {
    printf("%i ", array[n]);
}
printf("\n");