@Humbert
2017-10-02T14:40:17.000000Z
字数 5284
阅读 1209
所有I/O设备都被模型化为文件, 因此所有输入输出都被当作文件读写, 也因此所有输入输出都可以用一致的方式(Unix I/O)来执行:
1. 打开文件
通过内核来打开一个文件,并返回相应的文件描述符.内核记录文件的所有信息打开文件的应用程序只需要记住描述符.
每个进程一开始就有三个描述符:0(标准输入),1(标准输出),2(标准错误),因此其他描述符从3开始.
2. 改变当前文件的字节偏移量
将文件偏移量改为应有的偏移量.
3. 读写文件
读文件:从文件复制字节到内存,大于文件大小时返回EOF.
写文件:从内存复制字节到文件,然后更新字节偏移量.
4. 关闭文件
内核关闭文件,释放打开文件时用的数据结构,使描述符重新可用.
当进程中止时,内核都会关闭其打开的所有文件.
打开文件
int open(char *filename, int flags, mode_t mode)
flags参数:
可以使用|
符号来一起使用多个参数.
参数 | 意义 |
---|---|
O_RDONLY | 只读 |
O_WRONLY | 只写 |
O_RDWR | 读写 |
O_CREAT | 若不存在创建一个截断的空文件 |
O_TRUNC | 若文件已存在就截断 |
O_APPEND | 每次写之前将文件位置到文件结尾 |
文件截断 : 有时候我们需要在文件尾端处截取一些数据以缩短文件。
mode参数:若为创建一个新文件(O_CREAT), 则可以指定访问权限.
关闭文件使用描述符即可关闭.
int close(int fd);
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t n);
将文件中n个字符读到buf中
ssize_t write(int fd, const void *buf, size_t n);
将buf中n个字符写至fd所指文件中.
ssize_t 为有符号大小
size_t 为无符号大小
直接在文件与内存之间传输数据
在将二进制数据读写至网络和从网络读写二进制数据时很有用
ssize_t rio_readn(int fd, void *usrbuf, size_t n);
ssize_t rio_writen(int fd, void *usrbuf, size_t n);
在rio_t 结构体中的 rio_buf中存作为缓冲,之后再从中读取出来.
rio_t 结构体
#define RIO_BUFSIZE 8192
typedef struct {
int rio_fd; //与缓冲区绑定的描述符
int rio_cnt; //缓冲区还未读的字节数
char * rio_bufptr; //缓冲区中下一个要被读的字节的指针
char rio_buf[RIO_BUFSIZE]; //缓冲区
}rio_t;
rio_readinitb函数 : 绑定fd与rio_t 结构体.
void rio_readinitb(rio_t *rp, int fd)
{
rp -> rio_fd = fd;
rp -> rio_cnt = 0; //还未读的字节初始化为0
rp -> rio_bufptr = rp -> rio_buf; //将读取的指针指向缓冲区
}
rio_read函数
先从文件读到rio_t结构体缓存区,再memcpy到用户内存.
因为这个函数定义的输入输出与Linux系统中的read函数一致,所以用它来替换上文无缓冲输入输出中的read函数即可得到他的有缓冲版本->rio_readnb.
static ssize_t rio_read(rio_t * rp, char *usrbuf, size_t n)
{
int cnt;
while(rp->rio_cnt <= 0){ //若没有读到数据,则继续
rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, sizeof(rp->rio_buf));
//将fd所指的文件传送sizeof(rp->rio_buf)个字符到rp->rio_buf所指的空间中.若成功则返回字符数给rio_cnt, 出错返回-1,无可读数据返回0.
if(rp -> rio_cnt < 0){ //对返回为-1作处理
if(errno != EINTR)
return -1;
}
else if(rp->rio_cnt == 0) //对未读做处理
return 0;
else
rp->rio_bufptr = rp -> rio_buf; //一切正常则将读数据的指针指向保存数据的内存
}
/*从rio_t结构体中的缓存中复制 min(n, rp->rio_cnt) 个字节到用户内存中*/
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'){ //是换行符号则字符数+1并停止
n++;
break;
}
}
else if(rc == 0){
if(n == 1)
return 0;
else
break;
}
else
return -1;
}
*bufp = 0; //???
return n-1; //因为n从1开始,故n-1才为字节数目.
}
元数据指用来表示数据的数据
int stat(const char *filename, struct stat *buf);//以文件名作为输入
int fstat(int fd, struct stat *buf); //以文件描述符作为输入
他们会将文件转换为如下结构体,这些也就是所谓文件元数据
struct stat {
...
...
ino_t st_ino; /*inode*/
mode_t st_mode; /*文件类型与文件访问许可位*/
nlink_t st_nlink; /*hard links的number*/
uid_t st_uid; /*User ID of owner*/
gid_t st_gid; /*Group ID of owner*/
...
off_t st_size; /*Total Size in bytes*/
...
...
}
inode表示文件位置, 更多关于inode可以看inode-Wikipedia
st_mode可以用以下宏谓词来确定文件类型:
S_ISREG(m) -> m是一个普通文件吗?
S_ISDIR(m) -> m是一个目录文件吗?
S_ISSOCK(M) -> m是一个套接字吗?
opendir 函数
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
成功返回指向目录流的指针,出错返回NULL
流 是对条目有序序列的一个抽象,这里指目录项的列表
readdir 函数
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
若成功,返回返回下一个目录项的指针
若在该目录下没有更多的目录项或出错了,则返回NULL,并修改errno的值
每个目录项的结构如下:
struct dirent {
ino_t d_ino; /*inode number,文件位置*/
char d_name[256]; /*filename*/
};
closedir函数
#include <dirent.h>
int closedir(DIR *dirp);
关闭目录流并释放资源.
大体来说,读取目录内容的流程为:
1,使用opendir来得到一个指向目录流的指针.
2,使用readdir来一个个读取目录中的所有项目直到返回NULL.
3,再用closedir来关闭目录流.
int main(int argc, char **argv)
{
DIR *streamp;
struct dirent *dep;
streamp = Opendir(argv[1]);
errno = 0;
while((dep = readdir(streamp)) != NULL){
printf("Found file: %s\n", dep->d_name);
}
if(errno != 0)
unix_error("readdir error");
Closedir(streamp);
exit(0);
}
内核用三个相关的数据结构来表示打开的文件
注1:tty可以理解为一个交互环境,如终端.
注2:
On Linux, the set of file descriptors open in a process can be
accessed under the path /proc/PID/fd/, where PID is the process
identifier.
调用fork()前只有父进程表指向文件表的指针,调用fork后子进程增加了对于文件表中相应文件的引用次数.而从file table到vnode table的引用是不受影响的.
在shell中,可用 >
符号来重定向输出到文件.
另一种方式是使用dup2函数
#include <unistd.h>
int dup2(int oldfd, int newfd);
成功返回非负描述符, 出错返回-1.
dup2()函数复制该进程的描述符表中的oldfd的表项到newfd表项.覆盖newfd之前的内容.
如果newfd已经打开了,dup2()会在复制oldfd之前关闭newfd.
.
如图,dup(4,1)会将标准输出(fd = 1)重定向到(fd = 4),即指向fileB.
之后fileA将会被关闭, file table到v-node table的引用会被删除, v-node table中对应项也会被释放.
包括:
#include <stdio.h>
extern FILE *stdin; 文件描述符为0
extern FILE *stdout; 文件描述符为1
extern FILE *stderr; 文件描述符为2
因为Linux中一切皆文件,所以一开始也打开stdin, stdout, stderr三个文件.
FILE类型的流是对文件描述符和流缓冲区(使开销较大的Linux I/O函数调用次数尽可能小)的抽象.
本章讨论的所有I/O函数如下:
可以看出标准I/O函数与RIO函数都是基于Unix I/O函数来实现的.
限制二:跟在输入函数后的输出函数:若之间没有fseek, fsetpos, rewind的调用,且输入函数又不是自然结束,则不可在其后跟输出函数.
而因为lseek函数在套接字中为非法的,因此不建议在网络套接字的读取中使用标准I/O函数,而是建议使用RIO函数.可用sprintf在内存中格式化一个字符串, 再用rio_writen写.或是用rio_readlineb读取一个文本行,再用sscanf从文本行提取不同字符串.