系统级I/O
在unix系统中,要对一个文件进行操作,步骤为先根据文件名打开文件,得到一个文件描述符(通常为非负整数),然后根据文件描述对文件进行I/O操作,之后关闭文件。文件操作在系统中是这样的,每个进程都有一个打开文件表t1,系统中有两个所有文件共享的表t2,以及v1。
(1)表t1表示一个进程打开的所有文件,所以当一个进程fork一个子进程时,子进程会获得一个t1的拷贝,即从被fork开始,子进程就已经打开了父进程所打开的所有文件。当一个进程成功打开一个文件时,t1增加一项,t1项的内容为文件描述符,以及一个指向t2项的指针,以及文件指针的位置(即读写到第几个位置了)。
(2)表t2表示当前系统中所有打开的文件,记录着文件名,文件目录,引用次数(即有多少个文件描述符映射到了这个文件),当引用数量为0时删除该项,另外还有一个指向v1表项的指针。
(3)表v1记录了文件的具体信息,权限控制,文件类型(硬件设备还是socket或者是普通文件),v1表也是所有进程共享的。即大多数调用stat和fstat能得到的信息都有。
打开/关闭文件由下列函数完成:
int open(char *filename, int flags, mode_t mode); //flags表示打开方式,mode表示新建文件的默认权限,成功则返回描述符,否则返回-1 int close(int fd); //成功则返回0,否则返回-1
所以同一个进程对一个文件打开多少次都没问题,不同的文件描述符可以指向同一个文件,关键是他们的文件指针的位置可以不同。另外要注意的是进程A打开了文件a,得到描述符fda,然后A fork出一个子进程B,那么B的文件表会与A的文件表一样,即相同的fda,指向相同的文件a,所以只有A,B都关闭了文件fda之后系统才有可能回收对应的系统文件表项(当然其他进程没关对应的文件的话,系统是不会回收表项的)
系统I/O与标准I/O
很明显,C/C++的标准I/O就是对系统I/O的封装。多数时候直接使用标准的输入输出以及文件流就可以满足我们的需求。但是在读写socket的时候标准输入输出流则会有很多限制。
因为socket是全双工的,但是对同一个流的交错的读写操作必须在中间穿插一个刷新缓冲区或者移动文件指针的操作即:
(1)根在输出函数后的输入函数必须插入fflush,fseek,fsetpos或者rewind,第一个函数刷新缓冲区,后面三个使用lseek重置文件指针位置。
(2)在输入函数之后的输出函数必须插入fseek,fsetpos或者rewind,除非输入函数遇到了EOF
引起这些的原因都是因为标准输入输出为了提升性能减少I/O次数,设置了乱七八糟的缓冲区才会有了这种奇怪的限制。
然而socket文件是不能使用lseek的,虽然第一条规则可以在每次输出后刷新缓冲区,但是我们无法在每次输入后使用fseek,fsetpos,rewind。解决第二条冲突的办法是,我们可以为同一个socket打开两个流,一个用于输入一个用于输出,如:
int finput = fopen(sockfd, "r"); int foutput = fopen(sockfd, "w");
但是这么一来会有内存泄露。为了让系统释放对应的数据结构需要将两个流都关闭,但是这样会两次关闭同一个socket,所以第二个会关闭失败。更危险的是在多线程编程中,会把别人的socket也一块给关了。所以在socket编程中一般不适用标准输入输出,而使用系统函数,为了提升读写性能,当然是自己封装一个线程安全,带有缓冲区的系统读写函数了。
下面是摘自CSAPP中的rio读写包:
读函数会尽量读完n个字符,遇见EINTR中断会重启系统的read函数接着读,遇见EOF则返回已经读取的字符数
写函数一定会写完n个字符,除非遇到错误
这样封装之后,就不用每次都去判断是否读完,写完,如果没读完并且没遇到EOF,函数会自动接着读写
(1)无缓冲区的读写函数
// 无缓冲区的读写函数 ssize_t rio_readn(int fd, void *usrbuf, size_t n){ size_t nleft = n; char *bufp = usrbuf; while(nleft > 0){ if ((nread = read(fd, bufp, nleft)) < 0) { if (errno == EINTR) { nread = 0; } else { return -1; } }else if (nread == 0) { break; } nleft -= nread; bufp += nread; } return (n - nleft); } 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) { nwritten = 0; } else { return -1; } } nleft -= nwritten; bufp += nwritten; } return n; }
(2)带缓冲的读函数
//缓冲区定义 #define RIO_BUFSIZE 8192 typedef struct { int rio_fd; int rio_cnt; char *rio_bufptr; char rio_buf[RIO_BUFSIZE]; }rio_t; void rio_readinitb(rio_t *rp, int fd) { rp->rio_fd = fd; rp->rio_cnt = 0; rp->rio_bufptr = rp->rio_buf; } //带缓冲的read函数,是实现rio_readlineb、rio_readnb的关键 static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n) { while (rp->rio_cnt <= 0) { rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, sizeof(rp->rio_buf)); if (rp->rio_cnt < 0) { if (errno != EINTR) { return -1; } } else if (rp->rio_cnt == 0) { return 0; } else { rp->rio_bufptr = rp->rio_buf; } } int cnt = n; if (rp->rio_cnt < n) { cnt =rp->rio_cnt; } memcpy(usrbuf, rp->rio_bufptr, cnt); rp->rio_bufptr += cnt; rp->rio_buf -=cnt; return cnt; } 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; } else { break; } } else { return -1; } } *bufp = 0; return n; } ssize_t rio_readnb( rio_t *rp, void *usrbuf, size_t n) { size_t nleft = n; size_t nread; char *bufp = (char *)usrbuf; while (nleft > 0) { if ((nread = rio_read(rp, bufp, nleft) < 0) { if (errno == EINTR) { nread = 0; } else { return -1; } } else if (nread == 0) { break; } nleft -= nread; bufp += nleft; } return (n - nleft); }
带缓冲的读函数提升了读的性能,并且是线程安全的,因为他要求每个线程都提供一个独立的缓冲区。在<<uinx 网络编程>>书中,这两个函数使用static的全局缓冲区,所以是线程不安全的,让这类使用全局变量的函数变得可重入基本上都是让不同调用者使用不同缓冲区,比如rio包,就是给了缓冲区定义,让不同调用者都自己弄个缓冲区,另外在函数里边动态new一个,或者加个锁什么的都是可以的
浙公网安备 33010602011771号