Skip to content

L16 System-Level I/O

Source: https://www.bilibili.com/video/BV1iW411d7hd?p=16

Today

​ Unix I/O

​ RIO (robust I/O) package

​ Metadata, sharing, and redirection

​ Standard I/O

​ Closing remarks


Unix I/O Overview

​ Unix 文件:万物皆文件,文件皆字节序列,不区分文件类型。

​ A Linux file is a sequence of m bytes,

\(B_0, B_1,\cdots, B_k,\cdots,B_{m-1}\).

​ Cool fact: All I/O devices are represented as files:

/dev/sda2 (/usr disk partition) (sda 表示套接字(socket),网络连接后,通过互联网进行通信,实际上是通过写入套接字来发送信息,并从套接字读出接收的信息的;低级 API 中也是通过在磁盘驱动器上读写文件以及通过互联网发送数据包。)

/dev/tty2 (terminal) (tty 表示电传打字机 TeletTYpe,过去人们用它来描述打字机与计算机的接口)

​ Even the kernel is represented as a file:

/boot/vmlinuz-3.13.0-55-generic (kernel image)

/proc (kernel data )

​ Elegant mapping of files to devices allows kernel to export simple interface called Unix I/O.

​ Opening and closing files

open() and close()

​ Reading and writing a file.

read() and write()

​ Changing the current file position (seek)

​ 当前文件位置(current file position):表示当前读到的文件内容的位置。

​ indicates next offsets into files to read or write.

lseek()

\(B_0, B_1,\cdots, B_k\leftarrow ,\cdots,B_{m-1}\),current file position = k.

File Types

​ Each file has a type indicating its role in the system.

​ 普通文件 regular file: Contains arbitrary data.

​ 目录 Directory: Index for a related group of files.

​ 套接字 Socket: For communicating with a process on another machine.

​ Other file types beyond our scope.

​ 管道文件 Named pipes (FIFOs) 这是一类特殊的文件,既是前一程序的输入也是后一程序的输入。

​ 符号链接 Symbolic links 这是一类特殊的文件,不需要创建副本就可以有多个名称,被多个指针指向。

Character and block devices.

Regular Files

​ A regular file contains arbitrary data.

​ Applications often distinguish between text files and binary files.

​ 文本文件 Text files are regular files with only ASCII or Unicode characters.

​ 二进制文件 Binary files are everything else. e.g. object files, JPEG images.

​ Kernel doesn't know the difference!

​ Text file is sequence of text lines.

​ Text line is sequence of chars terminated by newline char ('\n')

​ Newline is 0xa, same as ASCII line feed character (LF).

​ End of line (EOL) indicators in other systems

​ Linux and MacOS: '\n'(0xa): line feed (LF)

​ Windows and Internet protocols: \r\n(0xd 0xa): Carriage return (CR) followed by line feed (LF).

老式打字机(old-fashioned typewriter):打字机左侧是一个滚筒(bar),当你打字时,纸就会随着滚筒向左平移,当滚筒移动到末端时,需要回到下一行的开头,这时候既需要回车(把滚筒推到右边,回到该行行首,carriage return),也需要换行(让滚筒切换到下一行,这一步需要手动转旋钮完成,line feed)。

Directories

​ Directory consists of an array of links.

​ Each link maps a filename to a file.

​ Each directory contains at least two entries.

. (dot) is a link to itself.

.. (dot dot) is a link to the parent directory in the directory hierarchy (next slide).

​ Commands for manipulating directories

mkdir: create empty directory.

ls: view directory contents.

rmdir: delete empty directory.

目录层次结构 Directory Hierarchy

​ All files are organized as a hierarchy anchored by root directory named / (slash)

​ Kernel maintains 当前工作目录 current working directory (cwd) for ea??

​ Modified using the cd command.

目录又是指向其子目录的指针,而子目录也是文件。

路径名 Pathnames

​ Locations of files in the hierarchy denoted by pathnames.

​ 绝对路径 Absolute pathname starts with / and denotes path from root.

​ e.g. /home/droh/hello.c

​ 相对路径 Relative pathnames denotes path from current working directory.

​ e.g. ../droh/hello.c where cwd is /home/bryant.

还可以用 ~(tilde) 或 ~/ 来指定相对于用户目录的路径名。

利用文件名对文件进行打开(open)、读(read)、写(write)、关闭(close)操作,而这些程序本质上还是一些文件。

Opening Files

​ Opening a file informs the kernel that you are getting ready to access the file.

int fd;    /* file descriptor */

if ((fd = open("/etc/hosts", O_RDONLY)) < 0) {
    perror("open");
    exit(1);
}

此处打开文件,除了指定路径名,还需要一个整数表明打开方式,此处 O_RDONLY 即以只读方式打开。这些整数由一系列预先定义好的常量指定,但是你要 include 相关文件,如果没有,可以自己查对应整数。

​ (打开操作)返回一个文件描述符。Returns a small identifying integer file descriptor.

fd == -1 indicates that an error occurred,

文件描述符是一个较小的整数,并且顺序递增,其存在上限,即限制了同时打开的文件之数量。使用 limit 命令查看 descriptors 一栏,得到这个上限(一般是 1024)。

我们应当总是要检查返回值,以避免错误带出新错误。但更多人不检查,只是忽略它并祈祷不要出错。

​ Each process created by a Linux shell begins life with three open files associated with a terminal:

​ 0: standard input (stdin)

​ 1: standard output (stdout)

​ 2: standard error (stderr)

Closing Files

​ Closing a file informs the kernel that you are finished accessing that file.

int fd;     /* file descriptor */
int retval; /* return value */

if ((retval = close(fd)) < 0) {
    perror("close");
    exit(1);
}

关闭文件也能出错?能的,特别是在多线程程序中。

​ 在多线程程序中关闭一个已经关闭的文件显然是一场灾难。Closing an already closed file is a recipe for disaster in threaded programs (more on this later).

​ Moral: Always check return codes, even for seemingly benign function such as close().

Reading Files

​ Reading a file copies bytes from the current file position to memory, and then updates file position.

char buf[512];
int fd;        /* file descriptor */
int nbytes;    /* number of bytes read */

/* Open file fd ... */
/* Then read up to 512 bytes from file fd */
if ((nbytes = read(fd, buf, sizeof(buf))) < 0) {
    perror("read");
    exit(1);
}

在低级 I/O 中,读函数就是 read(),调用该函数需要一个文件描述符,一个指向缓冲区的指针,和指定的缓冲区的长度以避免溢出。缓冲区可以通过静态分配或 malloc 分配得到。读入字符需至少一个,并且一定不会超过缓冲区长度。当读入的字节数小于指定的最大值时,称为不足值(short read),需要注意这种情况。

​ Returns number of bytes read from file fd into buf.

​ Return type ssize_t is signed integer.

nbytes < 0 indicates that an error occurred.

Short counts (nbytes < sizeof(buf)) are possible and errors.

Writing Files

​ Writing a file copies bytes from memory to the current file position, and then updates current file position.

char buf[512];
int fd;        /* file descriptor */
int nbytes;    /* number of bytes write */

/* Open file fd ... */
/* Then write up to 512 bytes from file fd */
if ((nbytes = write(fd, buf, sizeof(buf))) < 0) {
    perror("write");
    exit(1);
}

Simple Unix I/O example

​ Copying stdin to stdout, one byte at a time.

#include "csapp.h"

int main(void) {
    char c;
    while(Read(STDIN_FILENO, &c, 1) != 0)
        Write(STDOUT_FILENO, &c, 1);
    exit(0);
}

这样的代码确实可以,但每次只操作一个字节将会导致连绵不断的上下文切换,对于中等大小的文件,时间开销就已经相当巨大。

关于不足值 On Short Counts

​ Short counts can occur in these situations:

​ Encountering (end-of-file) EOF on reads.

​ EOF 的情形是这样的,假设还有 100 个字节,要读入 200 个字节,先读入 100 个字节,给出一个不足值,返回值为 100;再单独读 EOF,返回值为 0,也是不足值。

​ Reading text lines from a terminal.

​ Reading and writing network sockets.

​ 终端读入和网络读入其实是一样的,对于输入实际上都拆成了小包:终端拆解为每一行,网络拆解为数据包,数据包大小取决于具体协议,通常约 1000 字节。

​ Short counts never occur in these situations:

​ Reading from disk files (except for EOF).

​ Writing to disk files.

​ Best practice is to always allow for short counts.

The RIO Package

​ RIO is a set of wrappers that provide efficient and robust I/O in apps, such as network programs that are subject to short counts.

​ RIO provides two different kinds of functions.

​ Unbuffered input and output of binary data.

rio_readn and rio_writen.

​ 不带缓冲的读写,这些函数直到读取/写入要求的字节数之前不会停止,若不足则会暂时挂起。因此若出现预期外的 EOF 将给出一个错误。

​ Buffered input of text lines and binary data.

rio_readlineb and rio_readnb.

​ Buffered RIO routines are thread-safe and can be interleaved arbitrary on the same descriptor.

​ 带缓冲的读写,有两种,一种基于文本,一种基于字节。

​ Download from [link]

​ [link]

Unbuffered RIO Input and Output

​ Same interface as Unix read and write.

​ Especially useful for transferring data on network sockets.

#include "csapp.h"

ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);

(Return: num. bytes transferred if OK, 0 on EOF (rio_readn only), -1 on error)

rio_readn returns short count only if encounters EOF.

​ Only use it when you know how many bytes to read.

rio_writen never returns a short count.

​ Calls to rio_readn and rio_writen can be interleaved and the same descriptor.

Implementation of rio_readn

/*
 * rio_readn - Robustly read n bytes (unbuffered)
 */
ssize_t rio_readn(int fd, void *usrbuf, size_t n) {
    size_t nleft = n;
    ssize_t nread;
    char *bufp = usrbuf;

    while (nleft > 0) {
        if ((nread = read(fd, bufp, nleft)) < 0) {
            if (errno == EINTR) /* Interrupted by sig handler return */
                nread = 0;      /* and call read() again */
            else                
                return -1;      /* errno set by read() */
        } else if (nread == 0)
            break;              /* EOF */
        nleft -= nread;
        bufp += nread;
    }
    return (n - nleft);         /* Return >= 0 */
}

Buffered RIO Input Functions

​ Efficiently read text lines and binary data from a file partially cached in an internal memory buffer.

#include "csapp.h"

void rio_readinitb(rio_t *rp, int fd);

ssize_t rio_readllineb(rio_t *rp, void *usrbuf, size_t maxlen);
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n);

(Return: num, bytes read if OK, 0 on EOF, -1 on error).

rio_readlineb reads a text line of up to maxlen bytes from file fd and stores the line in usrbuf.

​ Especially useful for reading text lines from network sockets.

​ Stopping conditions

maxlen bytes read.

​ EOF encountered.

​ Newline ('\n') encountered.

rio_readnb reads up to n bytes from file fd.

​ Stopping conditions

maxlen bytes read.

​ EOF encountered.

​ Calls to rio_readlineb and rio_readnb can be interleaved arbitrary on the same descriptor.

​ Warning: Don't interleave with calls to rio_readn.

Buffered I/O: Implementation

​ For reading from file.

​ File has associated buffered to hold bytes that have been read from file but not yet read by user code.

Buffer             |<- rio_cnt ->|
--------------------------------------------
| already read     | unread      |         |
--------------------------------------------
^                  ^
|                  |
rio_buf            rio_bufptr

​ Layered on Unix file:

                 |<------ buffered portion ------>|
-------------------------------------------------------------
| not in buffer  | already read     | unread      |         |
-------------------------------------------------------------
                                                  ^
                                                  |
                              current file position

Buffered I/O: Declaration

​ All information contained in struct.

Buffer             |<- rio_cnt ->|
--------------------------------------------
| already read     | unread      |         |
--------------------------------------------
^                  ^
|                  |
rio_buf            rio_bufptr
typedef struct {
    int rio_fd;                /* descriptor for this internal buf */
    int rio_cnt;               /* unread bytes in internal buf */
    char *rio_bufptr;          /* next unread byte in internal buf */
    char rio_buf[RIO_BUFSIZE]; /* internal buffer */
} rio_t;

RIO Example

​ Copying the lines of a text file from standard input to standard output.

#include "csapp.h"

int main(int argc, char **argv) {
    int n;
    rio_t rio;
    char buf[MAXLINE];

    Rio_readinitb(&rio, STDIN_FILENO);
    while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
        Rio_writen(STDOUT_FILENO, buf, n);
    exit(0);
}

文件元数据 File Metadata

Metadata is data about data, in this case file data.

​ Per-file metadata maintained by kernel.

​ accessed by users with the stat and fstat functions.

/* Metadata returned by the stat and fstat function */
struct stat {
    dev_t         st_dev;     /* Device */
    ino_t         st_ino;     /* inode */
    mode_t        st_mode;    /* Pretection and filetype */
    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 */
    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 */
};

Example of Accessing File Metadata

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

    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_ISUSR))     /* Check and access */
        readok = "yes";
    else 
        readok = "no";

    printf("type: %s, read: %s\n", type, readok);
    exit(0);
}
linux> ./statcheck statcheck.c
type: regular, read: yes
linux> chmod 000 statcheck.c
linux> ./statcheck statcheck.c
type: regular, read: no
linux> ./statcheck ..
type directory, read: yes

How the Unix Kernel Represents Open Files

​ 这是一个难点。

QQ_1766073031815

File Sharing

QQ_1766073128411

How Processes Share Files: fork

QQ_1766073373417

QQ_1766073442184

I/O Redirection

​ Question: How does a shell implement I/O redirection?

linux> ls > foo.txt

​ Answer: By calling the dup2(oldfd, newfd) function.

​ Copies (per-process) descriptor table entry oldfd to entry newfd.

Descriptor table  ----------> Descriptor table 
before dup2(4,1)              after dup2(4,1)
     ------                        ------
fd 0 |    |                   fd 0 |    |
     ------                        ------
fd 1 |a   |                   fd 1 |b   |
     ------                        ------
fd 2 |    |                   fd 2 |    |
     ------                        ------
fd 3 |    |                   fd 3 |    |
     ------                        ------
fd 4 |b   |                   fd 4 |b   |
     ------                        ------

I/O Redirection Example

QQ_1766073890214

QQ_1766073925861

Standard I/O Functions

​ The C standard library (libc.so) contains a collection of higher-level standard I/O functions.

​ Documented in Appendix B of K&R.

​ Examples of standard I/O functions:

​ Opening and closing files (fopen and fclose)

​ Reading and writing bytes (fread and fwrite)

​ Reading and writing text lines (fgets and fputs)

​ Formatted reading and writing (fscanf and fprintf)

Standard I/O Streams

​ Standard I/O models open files as streams.

​ Abstraction for a file descriptor and a buffer in memory.

​ C programs begin life with three open streams (defined in stdio.h)

stdin (standard input), stdout (standard output), stderr (standard error).

#inlcude <stdio.h>
extern FILE *stdin;  /* standard input  (descriptor 0) */
extern FILE *stdout; /* standard output (descriptor 1) */
extern FILE *stderr; /* standard error  (descriptor 2) */

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

Buffered I/O: Motivation

​ Applications often read/write one character at a time

getc, putc, ungetc

gets, fgets

​ Read line of text one character at a time, stopping at newline.

​ Implementing as Unix I/O calls expensive

read and write require Unix kernel calls.

​ > 10,000 clock cycles.

​ Solutions: Buffered read.

​ Use Unix read to grab block of types.

​ User input functions take one byte at a time from buffer.

​ Refill buffer when empty.

Buffering in Standard I/O

QQ_1766074444405

Standard I/O Buffering in Action

​ You can see this buffering in action for yourself, using the always fascinating Linux strace program.

#include <stdio.h>

int main() {
    printf("h");
    printf("e");
    printf("l");
    printf("l");
    printf("o");
    printf("\n");
    fflush(stdout);
    return 0;
}
linux> strace ./hello
execve("./hello", ["hello"], [/* ... */]).
...
write(1, "hello\n", 6)               = 6
...
exit_group(0)                        = ?

Unix I/O vs. Standard I/O vs. RIO

QQ_1766074652687

为什么还要自己写输入输出函数呢?这是因为标准库提供的函数虽然非常适用于对终端或文件执行 I/O 操作,但不太适用于网络连接,因为其不是为网络连接设计的,所以不好用。所以课程提供的 RIO 函数主要是用于网络连接。

此外,带缓冲的 I/O 与 RIO I/O 不能很好地共存,因为它们各自有自己地缓冲区,彼此之间互不了解,还会相互干扰。切忌混用和匹配使用。

Pros and Cons of Unix I/O

​ Pros

​ Unix I/O is the most general and lowest overhead form of I/O.

​ All other I/O packages are implemented using Unix I/O functions.

​ Unix I/O provides functions for accessing file metadata.

​ Unix I/O functions are async-signal-safe and can be used safely in signal handlers.

​ Cons

​ Dealing with short counts is tricky and error prone.

​ Efficient reading of text lines requires some form of buffering, also tricky and error prone.

​ Both of these issues are addressed by the standard I/O and RIO I/O.

Pros and Cons of Standard I/O

​ Pros:

​ Buffering increases efficiency by decreasing the number of read and write system calls.

​ Short counts are handled automatically.

​ Cons:

​ Provides no function for accessing file metadata.

​ Standard I/O functions are not async-signal-safe, and not appropriate for signal handlers.

​ Standard I/O is not appreciate for input and output on network sockets.

​ There are poorly documented restrictions on streams that interact badly with restrictions on sockets (CS:APP3e, Sec 10.11)

Choosing I/O Functions

​ General rule: use the highest-level I/O functions you can.

​ Many C programmers are able to do all of their work using the standard I/O functions.

​ But, be sure to understand the functions you use!

​ When to use standard I/O

​ When working with disk or terminal files.

​ When to use raw Unix I/O

​ Inside signal handlers, because Unix I/O is async-signal-safe.

​ In rare cases when you need absolute highest performance.

​ When to use RIO

​ When you are reading and writing network sockets.

​ Avoid using standard I/O on sockets.

Aside: Working with Binary Files

​ Functions you should never use on binary files:

​ Text-oriented I/O such as fgets, scanf, fio_readlineb.

​ Interpret EOL characters.

​ Use functions like rio_readn or rio_readnb instead.

​ String functions

strlen, strcpy, strcat.

​ Interprets byte value 0 (end of string) as special.

For Further Information

​ The Unix bible:

​ W. Richard Stevens & Stephen A. Rago, Advanced Programming in the Unix Environment, 2nd Edition, Addison Wesley, 2005

​ Updated from Stevens's 1993 classic text.

​ The Linux bible:

​ Michael Kerrisk, The Linux Programming Interface, No Starch Press, 2010.

​ Encyclopedic and authoritative.

Funs with File Descriptors (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);
    return 0;
}

​ What would this program print for file containing "abc"?

Fun with File Descriptors (2)

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

​ What would this program print for file containing "abc"?