1 文件的读写位置
本质是一个整数。
每个打开的文件都有一个与其相关的文件读写位置,保存在文件表项中,用以记录从文件头开始计算的字节偏移量。
文件读写位置 通常是一个非负整数,用off_t类型表示,32位系统上被定义为long long int,而在64位系统上被定义为long long int。
open()一个文件时,除非指定了O_APPEND表示,否则文件读写位置一律被设为0,即文件首字节的位置。(读,默认即为文件首;写,要加O_APPEND来到文件尾)
每一次操作都从当前的文件读写位置开始,并根据所读写的字节数,同步增加文件读写位置,来为下一次读写做好准备。
因为文件读写位置是保存在文件表项而非v节点中的,因此通过多次打开同一个文件得到多个文件描述符,各自拥有各自的文件读写位置。(n次打开,就有n个文件表项,1个v节点)
2 文件顺序读写 随机读写
2.1 lseek()
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
功能: 人为调整文件读写位置
fd 文件描述符
offset 相对whence的偏移字节数
whence 偏移起点,可取以下值:
SEEK_SET 文件头(首字节)开始
SEEK_CUR 当前位置(最后被读写字节的下一个字节)开始
SEEK_END 从文件尾(最后一个字节的下一个字节)开始
返回值:成功返回调整后的文件读写位置,失败返-1
lseek()函数不仅可实现调整功能,还可返回文件相应属性:
lseek(fd, -7, SEEK_CUR); // 从当前位置向文件头偏移7字节
lseek(fd, 0, SEEK_CUR); // 返回当前文件读写位置
lseek(fd, 0, SEEK_END); // 返回文件总字节数(后续元数据部分,有另一种方式)
lseek()函数的功能仅修改保存在文件表项中的文件读写位置,并不实际发生任何I/O动作。
//lseek.c 文件读写位置
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
int main(void){
//打开文件
int fd = open("./lseek.txt",O_WRONLY | O_CREAT | O_TRUNC,0664);
if(fd == -1){
perror("open");
return -1;
}
//向文件中写入数据 hello linux!
char* buf = "hello linux!";
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//调整文件读写位置
if(lseek(fd,-6,SEEK_END) == -1){
perror("lseek");
return -1;
}
//再次向文件写入数据 world!
buf = "world!";
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//再次调整文件读写位置
if(lseek(fd,8,SEEK_END) == -1){
perror("lseek");
return -1;
}
//第三次写入
buf = "a";
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//关闭文件
close(fd);
return 0;
}
//执行后查看txt文件,在ubuntu和win下均为21B(数字0,即字符'\0',即文件空洞占了8B)。
//cat 查看结果:hello world!a
//ls-l 查看结果:hello world!^@^@^@^@^@^@^@^@a
//ubun 双击结果:hello world!/00/00/00/00/00/00/00/00a
//win 双击结果:hello world! a
//文件空洞用处:文件2G下载开始时,硬盘先形成2G空间的文件空洞,随着下载的进行,填充数据。
lseek()函数将读写位置设置为文件尾之后8字节,写入,关闭文件。txt文件生成文件空洞。
lseek()函数将读写位置设置为文件尾之后8字节,不执行写入,关闭文件。txt文件不生成文件空洞。
lseek()函数将读写位置设置为文件头之前8字节,编译成功,执行报错:invalid argument。
3 文件描述符(fd)的复制
3.1 dup()
#include <unistd.h>
int dup(int oldfd);
功能:复制文件描述符表的特定条目到最小可用项(无法指定具体数值)
oldfd:源文件描述符
返回值:成功返回目标文件描述符,失败返-1。
dup()函数将oldfd所对应的文件描述符表项复制到文件描述符表项第一个空闲项中,同时返回该表项对应的文件描述符。
dup函数返回的文件描述符一定是调用进程当前未使用的最小文件描述符。
dup函数只复制文件描述符表项,不复制文件表项和v节点,因此该函数所返回的文件描述符可以看做是文件描述符oldfd的副本,他们标识同一个文件表项,如下图。
当关闭文件时,即使是由dup函数产生的文件描述符副本,也应该通过close函数关闭。因为只有当关联于一个文件表项的所有文件描述符都被关闭了,该文件表项才会被销毁。
类似的,只有当关联于一个v节点的所有文件表项都被销毁了,v节点才会被从内存中删除。因此从资源合理利用的角度讲,凡明确不再继续使用的文件描述符,都应及时用close函数关闭。
dup函数究竟会把oldfd参数所对应的文件描述符表项,复制到文件描述符表的什么位置,程序员是无法控制的,这完全由调用该函数时文件描述符表的使用情况决定,因此对该函数的返回值做任何约束性假设都是不严谨滴。
由上图可知,dup函数返回的文件描述符 与 作为参数传递给该函数的文件描述符 标识的是同一个文件表项,而文件读写位置是保存在文件表项而非文件描述符表项中的,因此通过这些文件描述符中的任何一个,对文件进行读写或随机访问,都会影响 通过其他文件描述符操作的文件读写位置。这与多次通过open函数打开同一个文件不同。
3.2 dup2()
#include <unistd.h>
int dup2(int oldfd, int newfd);
功能:复制文件描述符表的特定条目到指定项(可以指定值)
oldfd:源文件描述符
newfd:目标文件描述符
返回值:成功返回目标文件描述符(newfd),失败返-1。
dup2函数在复制 由oldfd参数所标识的源文件描述符表项时,会先检查由newfd参数所标识的目标文件描述符表项是否空闲。空闲则直接将前者复制给后者,否则先将目标文件描述符newfd关闭,使之成为空闲项(霸道),再复制。
dup和dup2()返回的newfd与oldfd共享同一个文件读写位置:
//dup.c fd的复制,共享文件读写位置
#include<stdio.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
int main(void){
//打开文件,得到文件描述符,oldfd
int oldfd = open("./dup.txt",O_WRONLY | O_CREAT | O_TRUNC,0664);
if(oldfd == -1){
perror("open");
return -1;
}
printf("oldfd = %d\n",oldfd);
//复制文件描述符,得到newfd
//int newfd = dup(oldfd); //dup()
int newfd = dup2(oldfd,STDOUT_FILENO); //dup2()
if(newfd == -1){
perror("dup");
return -1;
}
printf("newfd = %d\n",newfd);
//通过oldfd向文件写入数据 hello world!
char* buf = "hello world!";
if(write(oldfd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//通过newfd修改文件读写位置
if(lseek(newfd,-6,SEEK_END) == -1){
perror("lseek");
return -1;
}
//通过oldfd再次向文件写入数据 linux!
buf = "linux!";
if(write(oldfd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//关闭文件
close(oldfd);
close(newfd); //所有关联fd都关闭,该文件表项才关闭
return 0;
}
$gcc dup.c -o dup
$./dup
$oldfd = 3 //发现少输出了
$cat dup.txt //输出到1没错,但1不再指向stdout,而是指向dup.txt,即输出到了txt中
$newfd = 1
$hello linux!
$
4 访问测试
4.1 access()
#include <unistd.h>
int access(char const* pathname, int mode);
功能:判断当前进程 是否可对 某个给定的文件 执行某种访问。
pathname:文件路径
mode:被测试权限,可以取以下值
R_OK 可读吗
W_OK 可写否
X_OK 可执行否
F_OK 存在否
返回值:成0败-1。
//access.c 访问测试
#include<stdio.h>
#include<unistd.h>
int main(int argc,char* argv[]){ //由下知,argv[]有3个元素:"./access","hello.c",NULL
// ./access hello.c 执行语句,2个命令行参数 字符指针数组的最后一个元素是NULL
// 存在,输出文件hello.c能读,能写,不能执行
// 不存在,输出文件hello.c不存在
if(argc < 2){
fprintf(stderr,"请用正确用法:./access <文件>\n");
return -1;
/*为什么用fprint(),不用perror()?
perror()输出最近一次函数调用出错的信息
argc < 2不是函数调用,是程序员自己定义的一个出错条件*/
}
if(access(argv[1],F_OK) == -1){
printf("文件%s不存在\n",argv[1]);
}else{
printf("文件%s存在",argv[1]);
if(access(argv[1],R_OK) == -1){
printf("不可读,");
}else{
printf("可读,");
}
if(access(argv[1],W_OK) == -1){
printf("不可写,");
}else{
printf("可写,");
}
if(access(argv[1],X_OK) == -1){
printf("不可执行\n");
}else{
printf("可执行\n");
}
}
return 0;
}
//编译、执行即可
5 修改文件大小
5.1 truncate()
5.2 ftruncate()
#include <unistd.h>
int truncate(char const* path, off_t length);
int ftruncate(int fd, off_t length);
功能:修改指定文件的大小
path:文件路径 //常规表示文件的方法1
fd:文件描述符 //常规表示文件的方法2
length:文件大小
返回值:成0败-1
//trunc.c 修改文件大小
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
int main(void){
//打开文件
int fd = open("./trunc.txt",O_WRONLY | O_TRUNC | O_CREAT,0664);
if(fd == -1){
perror("open");
return -1;
}
//向文件中写入数据 abcde
char* buf = "abcde";
if(write(fd,buf,strlen(buf)) == -1){ //5B abcde
perror("write");
return -1;
}
//修改文件大小
if(truncate("./trunc.txt",3) == -1){ //3B abc
perror("truncate");
return -1;
}
//再次修改大小
if(ftruncate(fd,5) == -1){ //5B abc^@^@
perror("ftruncate");
return -1;
}
//关闭文件
close(fd);
return 0;
}
//编译执行
这两个函数既可以把文件截短(不可逆,变长补空洞),也可以把文件加长。所有改变均发生在文件尾部,新增部分用数字0(字符 '\0' vi显示^@ gedit显示\00 win显示空格)填充。