Try   HackMD

LINUX 核心設計 課程用書筆記

contributed by < rwe0214 >

tags: Linux, rwe0214, CS:APP, NCKU

CS:APP Ch10 System-level I/O

Unix 中的 file 是一序列的 bytes 所組成,所有的 I/O ( e.g. networks, disks, terminals, ) 皆 model as file
因為這樣 Unix kernel 就可提供一個simple, low-level application interface ( Unix I/O ),達到驅動所有 I/O 一個統一和方便的方法。

10.4 Robust Reading and Writing with the Rio Package

因為 readwrite 的回傳值為已讀寫完的 bytes 數量,但在連線不穩定(例如網路 socket connect 等等)的情況下,不一定只執行一次 read/write 就可達到預期讀寫完 n bytes 的結果( i.e. short count > 0 ),所以需要基於使用 read/write 來設計更穩定的 I/O function,可分成 unbufferedbuffered 兩種。

Unbuffered I/O Function

利用回傳 short count 數量來判斷是否已經讀取完所需的 n bytes,若遇到 EOF 則回傳 0
rio_readn and rio_writen 如下:

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 */ }

可將

if ((nread = read(fd, bufp, nleft)) < 0) {

替換成

if ((nread = rio_read(fd, bufp, nleft)) < 0) {

即完成 buffered 版本,在下部分的 buffered I/O 會說明。

ssize_t rio_writen(int fd, void *usrbuf, size_t n) { size_t nleft = n; ssize_t nwritten; char *bufp = usrbuf; while (nleft > 0) { if ((nwritten = write(fd, bufp, nleft)) <= 0) { if (errno == EINTR) /* interrupted by sig handler return */ nwritten = 0; /* and call write() again */ else return -1; /* errno set by write() */ } nleft -= nwritten; bufp += nwritten; } return n; }

Buffered I/O Function

如果要計算 file 中有幾行資料(number of '\n'),上述 unbuffered 的作法利用多次 read 一次讀取一個 byte 並檢查是否為 '\n',但此方法會需要多次的 system call 所以極度沒有效率,

e.g. file 只有一行,但是是長度非常長的一行。

使用 rio_readlineb 來減少使用 read 的次數。
透過 subroutine rio_read 呼叫一次 read 讀取長度為 buflen 的資料後,再一個char 一個 char 比對是否為 \n,呼叫 system call 的數量便少了 buflen 倍,

以上面的例子來看,呼叫 read 的次數便有效的簡少了。

同樣的,可以將 rio_readn 擴展成 rio_readnb ,流程如下,

file → rio_buf → usrbuf(char) → usrbuf(char *)
|  system call |              |              |
|=====read=====|              |              |
|-----------rio_read----------|              |
|---------------rio_readlineb----------------|
or
|  system call |              |              |
|=====read=====|              |              |
|-----------rio_read----------|              |
|---------------rio_readnb-------------------|

為了達到以上的目標,定義了一個 rio_p 的結構體,

#define RIO_BUFSIZE 8192 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_readinitb

void rio_readinitb(rio_t *rp, int fd) { rp->rio_fd = fd; rp->rio_cnt = 0; rp->rio_bufptr = rp->rio_buf; }

再來就是介在中間的 rio_read

rio_readn 中的 read 替換成 rio_read 就是 rio_readnb

static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n) { int cnt; while (rp->rio_cnt <= 0) { /* refill if buf is empty */ rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, sizeof(rp->rio_buf)); if (rp->rio_cnt < 0) { if (errno != EINTR) /* interrupted by sig handler return */ return -1; } else if (rp->rio_cnt == 0) /* EOF */ return 0; else rp->rio_bufptr = rp->rio_buf; /* reset buffer ptr */ } /* Copy min(n, rp->rio_cnt) bytes from internal buf to user buf */ cnt = n; if (rp->rio_cnt < n) cnt = rp->rio_cnt; memcpy(usrbuf, rp->rio_bufptr, cnt); rp->rio_bufptr += cnt; rp->rio_cnt -= cnt; return cnt; }

有了以上,就能完成 rio_readlineb

ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen) { int n, rc; char c, *bufp = usrbuf; for (n = 1; n < maxlen; n++) { if ((rc = rio_read(rp, &c, 1)) == 1) { *bufp++ = c; if (c == '\n') break; } else if (rc == 0) { if (n == 1) return 0; /* EOF, no data read */ else break; /* EOF, some data was read */ } else return -1; /* error */ } *bufp = 0; return n; }

自我檢查清單

  • tty 是什麼?緣起為何?對 UNIX 的影響又是?
  • 為何 socket 也是一種 file type 呢?具體使用方式為何? Ans
  • RIO package 提供 I/O 封裝的目的為何?對應到真實世界的應用為何?

Ans: 因為 STDIOsocket 有 documented restriction,所以在 network socket 上進行 I/O 用 RIO package 會更適合。

Restriction 1: Input functions following output functions. An input function cannot follow an output function without an intervening call to fflush, fseek, fsetpos, or rewind. The fflush function empties the buffer associated with a stream. The latter three functions use the Unix I/O lseek function to reset the current file position.

Restriction 2: Output functions following input functions. An output function cannot follow an input function without an intervening call to fseek, fsetpos, or rewind, unless the input function encounters an end-of-file.

  • Buffered I/O 的動機和效益為何? Ans
  • fork 系統呼叫後,child process 和 parent process 的檔案分享狀況為何?(提示:閱讀 man-pages)

Ans: child 和 parent 會共享同一個 file descriptor

The child process has its own copy of the parent's descriptors. These descriptors reference the same underlying objects, so that, for instance, file pointers in file objects are shared between the child and the parent, so that an lseek(2) on a descriptor in the child process can affect a subsequent read or write by the parent. This descriptor copying is also used by the shell to establish standard input and output for newly created processes as well as to set up pipes.
Ref: manual fork