【Unix】分章复习笔记之第三章 · 文件IO (2)
3.8 IO效率相关的问题
3.8.1 课本的示例代码的详细理解
#include "apue.h"
#include "myerr.h"
#define BUFFSIZE 4096
int main()
{
int n;
char buf[BUFFSIZE];
while((n=read(STDIN_FILENO,buf,BUFFSIZE))>0)
if(write(STDOUT_FILENO,buf,n)!=n)
err_sys("write error");
if (n<0)
err_sys("read error");
return 0;
}
此段代码目的是利用read和write函数通过标准输入输出的方式将文件复制。==注意,既然是标准输入输出,那么,运行此功能应使用重定向的方式否则,运行时会卡住别问我为什么知道。==为什么选择使用的BUFFZSIZE是4096?
测试所用的文件系统是linux ext4文件系统,其磁盘块的长度为4096字节。再增大缓冲区长度对时间基本无影响。
3.8.2 注意事项
- 所有常用的UNIX系统shell都提供一种方法:在标准输入上打开一个文件用于读,在标准输出上创建或重写一个文件。这样程序可以不必打开输入输出文件(此代码中就无open函数)且允许使用重定向。
- 进程终止时,UNIX的内核自动关闭进程的所有打开文件的文件描述符,因此不需要使用close函数
- 文本文件或二进制文件都可以使用本程序,因为对于UNIX内核而言,两者无区别
3.9 内核I/O的数据结构
3.9.1 进程表项
每个进程在进程表里都有一个记录表项,包含一张打开文件描述符表。每一项包含两个部分:
- 文件描述符标志
- 指向一个文件表项的指针
我们通常这样描述进程表项:
3.9.2 文件表项
内核为所有打开文件维持一张文件表。包含:
-
文件状态标志
-
当前文件偏移量
-
指向该文件v节点表项的指针
3.9.3 v节点结构
每个打开的文件设备都有一个v节点结构。v节点包含了
- 文件类型和对此文件进行各种操作的指针。(一般统一书写为v节点信息)
- 该文件的i节点(在v_data内包含)
i节点包含了:
- 文件所有者,文件长度,指向文件实际数据块在磁盘上的所在位置(一般写为i节点信息)
- i_vnode
一般我们这样表示:
注意:Linux没有使用v节点,而是使用了通用i节点的结构。虽然实现方式不同,但是在概念上,v节点与i节点是一样的,都指向文件系统特有的i节点结构。
- 不同的进程打开啊同一个文件时,fd标志可能不同,文件表项各自使用各自的。因为每个进程都可能有它自己对统一文件的当前文件偏移量。
- 每次write后,文件表项中的当前文件偏移量增加nbyte字节,即写入的字节数 。如果导致写入后的文件偏移量大于原文件长度,则i节点的当前文件长度更新为当前文件偏移量。
- 当用O_APPEND参数打开文件,文件表项中的文件状态标志也会随之变为O_APPEND,即执行写操作时,文件表项的当前文件偏移量先被设置为i节点的当前文件长度。
- lseek函数只修改当前问价偏移量,不会进行任何IO操作。
3.10 原子操作
3.10.1 多线程同时写问题
假设现存 两个进程,两个进程打开同一文件且没有使用O_APPEND标志(早期的UNIX没有此标志),则两者可能使用同一段代码:
if(lseek(fd,0,SEEK_END)<0)
err_sys("error");
if(write(fd,buf,100)!=100)
err_sys("error");
代码意为先定义到文件末尾,再写1600字节。假设文件长度为1500字节。现在有AB两个进程都干这事:
时间节点 | A | B |
---|---|---|
1 | 调用lseek函数使得A进程的文件偏移量为1500 | |
2 | 调用lseek函数使得B进程的文件偏移量为1500 | |
3 | 调用write函数,写100字节,则写后文件偏移量增加到1600 | |
4 | 调用write函数,写100字节,把B进程写入的100字节覆盖 |
出现的问题在于:逻辑上的操作为:先定位到文件末尾,再写。然而当两个步骤进行的中间发生了进程切换(A在时间节点1后切换到了B进程),就可能会出现上面的错误。
为了解决上述错误,O_APPEND参数提供了一种原子操作,使得打开文件后写操作一定出现在文件末尾。因此不必再用lseek定位。
3.10.2函数pread和pwrite
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
若成功,返回读到的字节数。如果在末尾,返回0;否则,返回-1
ssize_t pwrite(itn fd, void *buf, size_t nbytes, off_t offset);
若成功,返回已写的字节数;否则返回-1
首先,我们发现了一个收悉的面孔:offset重现江湖。上一次见到他是在lseek,表明设置的偏移量。这里也差不多:
offset:读取的起始地址的偏移量,读取地址=文件开始+offset。注意,执行后,文件偏移指针不变
调用pread并不相当于先lseek再read。因为,它们二者之间有重要差别:
- 调用pread时,无法中断其定位和读操作
- 不更新当前文件偏移量
pwrite同上
3.10.3 原子操作定义
原子操作指的是由多步组成的一个操作。如果该操作原子地执行,则要么不执行,要么执行到底。
3.11 函数dup和dup2
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
成功,返回新的文件描述符;否则返回-1
3.11.1 dup和dup2有啥区别
dup和dup2都是为了复制一个现有的文件描述符。
- dup函数只传入了一个文件描述符fd。它的返回值也是一个文件描述符。这个文件描述符是该进程表项里面所有文件描述符的最小值。
- dup2可以用fd2的参数值指定新的文件描述符的值。如果fd2已经打开,则先将其关闭。如果fd=fd2,则dup2返回fd2而不关闭它。这样fd2在进程调用exec时是打开状态。
- 这些函数返回的新的文件描述符与参数fd共享同一个文件表项。
注意,dup2函数是原子操作
3.11.2 代码示例
#include "apue.h"
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd ;
fd = open("/home/ubuntu/workspqce/TestText.txt",O_RDWR);
int newfd = dup(fd);
int newfd2 = dup2(fd,3);
printf("%d,%d\n",newfd,newfd2);
return 0;
}
3.11.3 看图
这里先假设3暂时没被占用(因为正常情况下,0,1,2都被占用了。还记得吗?shell打开了012并依次作为标准输入,标准输出,标准错误) 这时,执行dup(1),可以发现,fd1和fd3同时指向同一个文件表项。
3.12 函数sync,fsync,fdatasync
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
成功,返回0;否则,返回-1
void sync(void);
-
sync将所有修改过的块缓冲区排入写队列,然后返回。它并不等待实际写磁盘操作结束。系统守护进程会周期性的调用sync。
-
fsync对fd指定的文件起作用,且等写入磁盘操作结束后返回。它不仅 写入数据,还修改文件的属性。
-
fdatasync类似fsync,但不修改文件属性。
个人认为不是很重要
3.13 函数fcnt1
#include <fcnt1.h>
int fcnt1(int fd, int cmd, ...);
若成功,依赖于cmd;失败返回-1
第三个参数可以是一个整数,也可以是一个结构指针。主要有以下5个功能:
- 复制已有的文件描述符:F_DUPFD或F_DUPFD_CLOEXEC
- 获取/设置文件描述符:F_GETFD或F_SETFD
- 获取/设置文件状态标志:F_GETFL或F_SETFL
- 获取/设置异步I/O所有权:F_GETOWN或F_SETOWN
- 获取/设置记录锁:F_GETLK、F_SETLK或F_SETLKW
fcntl也可复制已有的文件描述符
‒ 调用dup(fd)等价于fcntl(fd, F_DUPFD, 0)
**‒ 调用dup2(fd, fd2)等价于先调用close(fd2)再调用fcntl(fd, F_DUPFD, fd2) **
用的不多??不清楚,感觉不是很容易考~~
3.14 /dev/fd
比较新的系统都会提供一个特殊的/dev/fd目录。其中的目录项为0,1,2…的文件。
打开文件/dev/fd/n 相当于复制文件描述符n。(前提是文件描述符n打开)。意味着进行如下两个操作,得到的结果没有区别:
fd = open("/dev/fd/0",mode);
fd = dup(0);
代码证明:
#include "apue.h"
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("/dev/fd/0", O_RDWR);
int fd1 = dup(0);
printf("fd = %d , fd1 = %d\n",fd,fd1);
return 0;
}
纯手码,很辛苦的。希望考试没事