第10章——系统级I/O笔记

本文详细介绍了Unix系统中的I/O操作,包括打开、关闭文件及读写文件的原理与函数使用。接着,深入探讨了RIO包,包括无缓冲和带缓冲的输入输出函数,阐述了如何利用缓冲区提高读写效率。最后,提到了元数据、共享和重定向,以及标准I/O和结束语。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Unix I/O

Linux文件就是一个m个字节的序列,所有的IO设备(例如网络、磁盘和终端)都被模型化为文件,
而所有的输入和输出都被当作对相应的文件进行读和写。这种将设备优雅的映射为文件的方式,
允许Linux内核引出一个简单、低级的应用接口,称为 Unix I/O
这使得所有的输入和输出都能以一种统一且一致的方式来执行。
1.打开文件
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。
内核返回一个小的非负整数,叫做描述符<file descriptor>,用于标识这个文件。
内核记录有关这个打开文件的所有信息,应用程序只需记住这个描述符。
Linux shell创建的每个进程开始时都有三个打开的文件:(需头文件<unistd.h>)
0<->STDIN_FILENO
1<->STDOUT_FILENO
2<->STDERR_FILENO
2.读写文件
3.关闭文件
通知内核关闭该文件,内核会释放文件打开时创建的数据结构,并将该描述符恢复到可用
的描述符池中。无论一个进程因何种原因终止,内核都会关闭所有打开的文件并释放
他们的内存资源。
1.打开文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(char *filename, int flags, mode_t mode);

返回:若成功则返回新文件描述符,失败返回-1。

返回的描述符总是在进程当前没有打开的最小描述符。

flags参数指明了进程打算如何访问这个文件:

可是以一个多个掩码的或。
O_RDONLY: 只读
O_WRONLY: 只写
O_CREAT : 如果文件不存在,就创建一个截断的(truncated)(空)文件。
O_TRUNC : 如果文件已存在,就截断它(长度被截为0,属性不变)
O_APPEND: 在每次写操作前,设置文件位置到文件的结尾
O_RDWR: 可读可写

mode参数指定了新文件的访问权限位。

2.关闭文件

close函数关闭一个打开的文件

#include <unistd.h>

int close(int fd);

                //返回: 若成功则为0,若出错则为-1

关闭一个已关闭的描述符会出错。

3.读和写文件
#include <unistd.h>

ssize_t read(int fd,void *buf,size_t n);
//read函数从描述符fd的当前文件位置拷贝最多n个字节到存储器buf

                    返回:若成功则为读的字节数,若EOF则为0,若出错为 -1.
ssize_t write(int fd,const void *buf,size_t n)
//write函数从存储器位置buf拷贝至多n个字节到描述符fd的当前文件位置

                    返回:若成功则为写的字节数,若出错则为-1
                   

ssize_t is long
size_t is unsigned long

read return:
return -1 :错误
return 0 :遇到EOF(end of file)
return >0:返回实际传送的字节数量
.
wrire return
return -1:错误
return >0:返回实际传送的字节数量

Short counts:不足值
在这里插入图片描述
比如规定max读200字节,实际文件只有100字节,那么第一次读的时候,会返回100,然后读进去。第二次读的时候会返回0,表示遇到EOF,即上次已经读到文件末尾了。

RIO Package

1.RIO无缓冲输入输出函数

rio_readnrio_writen都会一直循环地读或写,直到不足值被妥善处理。


rio_readn:从fd文件的当前位置最多传送n个字节到内存位置usrbuf,在遇到EOF只能返回一个不足值。

/*********************************************************************
 * The Rio package - robust I/O functions
 **********************************************************************/
/*
 * rio_readn - robustly read n bytes (unbuffered)
 */
/* $begin rio_readn */
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 */
	// 如果是被中断了造成的错误,read这种慢系统调用不会读
		nread = 0;      /* and call read() again */
	    else
	// 如果是别的错误,直接return -1.
		return -1;      /* errno set by read() */ 
	} 
	else if (nread == 0)
	    break;              /* EOF */ 
	// nread>0正常读了时,修改nleft(还需要读的字节数)和bufp
	nleft -= nread;
	bufp += nread;
    }
    // 读到EOF或者读了n个字节后,return (n - nleft)
    return (n - nleft);         /* return >= 0 */
}
/* $end rio_readn */

rio_readn中,会一直调用read,直到出错(除了EINTR外,若是EINTR会继续去调用read)或者读到EOF或者读了n个字节后才会停止。


rio_writen:从usrbuf传送n个字节到描述符fd,不会返回不足值。

/*
 * rio_writen - robustly write n bytes (unbuffered)
 */
/* $begin rio_writen */
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;       /* errorno set by write() */
	}
	nleft -= nwritten;
	bufp += nwritten;
    }
    return n;
}
/* $end rio_writen */

rio_writen中,与rio_readn非常像,会一直调用write,直到出错(除了EINTR外,若是EINTR会继续去调用read)或者写了n个字节后才会停止。

2.RIO带缓冲输入输出函数

在代码中建立一个小的缓冲区,用来存放已读但还未被应用程序使用的字节,或用来在程序中存放一些字节以便未来输出到文件或网络中。
在这里插入图片描述

如果用户程序想要从文件读取一些字节,会先看看缓冲区Buffer中是否还有未读取的字节,如果有就接着读,如果没有,就重新填满Buffer。好处就是不用每次需要一个或少量字符时,都调用一次操作系统,而是先调用把字符缓存起来,后续只需要从缓冲区读取,避免再去调用操作系统.


假设我们要编写一个程序来计算文本文件中文本行的数量,该如何实现呢?一种方法是用read函数来依次一个字节地从文件传送到用户内存,检查每个字节来查找换行符,这种方法显然效率极低。
更好的办法是调用一个包装函数(rio_readlineb),从一个内部读缓冲区复制一个文本行,当缓冲区变空时,自动调用read来重新填满缓冲区。


在这里插入图片描述

/* $begin cpfile */
#include "csapp.c"

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);
    /* $end cpfile */
    exit(0);
    /* $begin cpfile */
}
/* $end cpfile */

2.1 struct rio_t

/* Persistent state for the robust I/O (Rio) package */
/* $begin rio_t */
#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;
/* $end rio_t */

rio_fd:关联的描述符fd
char rio_buf[]: 缓冲区数组
rio_cnt:剩余可读字节数
rio_bufptr:下一次读时开始的指针

2.2 readinitb
初始化缓冲区 关联一个文件描述符fd

/*
 * rio_readinitb - Associate a descriptor with a read buffer and reset buffer
 */
/* $begin rio_readinitb */
void rio_readinitb(rio_t *rp, int fd) 
{
    rp->rio_fd = fd;  
    rp->rio_cnt = 0;  
    rp->rio_bufptr = rp->rio_buf;
}
/* $end rio_readinitb */

2.3 rio_read
这是Linux read函数的带缓冲的版本。当调用rio_read要求读n个字节时,读缓冲区内有rp->rio_cnt个未读字节。如果缓冲区为空 --> 通过read来填满它。
一旦缓冲区非空,rio_read就从读缓冲区复制nrp->rio_cnt中较小值个字节到用户缓冲区,
并返回复制的字节数。
在这里插入图片描述

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;
}
/* $end rio_read */

2.4 rio_readlineb
从文件rp读出下一个文本行(包括结尾的换行符),将它复制到usrbuf,并用NULL(零)字符来结束这个文本行。

/* 
 * rio_readlineb - robustly read a text line (buffered)
 */
/* $begin 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;
}
/* $end rio_readlineb */

2.5 rio_readnb

/*
 * rio_readnb - Robustly read n bytes (buffered)
 */
/* $begin rio_readnb */
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n) 
{
    size_t nleft = n;
    ssize_t nread;
    char *bufp = usrbuf;
    
    while (nleft > 0) {
	if ((nread = rio_read(rp, bufp, nleft)) < 0) {
	    if (errno == EINTR) /* interrupted by sig handler return */
		nread = 0;      /* 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 */
}
/* $end rio_readnb */

在有了上述的函数之后,就可以运行我们的main函数啦!

/* $begin cpfile */
#include "csapp.c"

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);
    /* $end cpfile */
    exit(0);
    /* $begin cpfile */
}
/* $end cpfile */

跟踪一下运行:
在这里插入图片描述
可以看到这种缓冲区的优势就是读写的次数很少。

Metadata,sharing and redirection

Standard I/O

Closing remarks

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值