今天继续来看看文件的东西
FILE结构体
C语言的stdio.h头文件中,定义了用于文件操作的结构体FILE。这样,我们通过fopen返回一个文件指针(指向FILE结构体的指针)来进行文件操作。可以在stdio.h(位于visual studio安装目录下的include文件夹下)头文件中查看FILE结构体的定义,如下:
- struct _iobuf {
- char *_ptr; //文件输出的下一个位置
- int _cnt; //当前缓冲区相对位置
- char *_base; //文件的开始位置
- int _flag; //文件标志
- int _file; //文件描述符表编号
- int _charbuf; //文件缓冲区
- int _bufsiz; //缓冲区大小
- char *_tmpfname;
- };
- typedef struct _iobuf FILE;
如果俩个进程同时打开同一个文件,系统调用方式执行:
1.每个进程都会独立创建一个文件描述符表,读取时互不影响。
2.以读写的方式打开,先读后写,会从读后的位置开始覆盖写入新的内容。
那么如果是标准IO的方式执行:
1.俩个进程都会将开始的文件全部读入到缓冲区。
2.当缓冲区满后,会再次从文件读取内容(可能会发生更新)。
3.因为俩个进程都同时读入了开始的内容,所以可以独立操作原来的内容。
4.文件的最终结果,受俩个程序的综合应用。
阻塞和非阻塞
阻塞
为了完成一个功能,发起一个调用,如果不具备条件的化则一直等待,直到具备条件则完成。
非阻塞
为了完成一个功能,发起一个调用,具备条件,直接输出,不具备条件则直接报错返回。
注意:对于非阻塞的使用必须使用循环进行调用。
区别:
其实就相当于在捕捉一个子进程退出的时候,阻塞则会一直等待,直到这个子进程退出,返回对应的值,而非阻塞,如果刚好捕捉到子进程的退出则直接输出, 如果没有捕捉到,也不进行等待,直接输出报错。
fcntl函数,修改文件属性
fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性。
函数原型:
#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);
fcntl函数功能依据cmd的值的不同而不同。参数对应功能如下:
重点掌握俩个函数参数的使用,F_GETFL和F_SETFL。【fcntl.c】
//查看tty设置为非阻塞属性,优化代码
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
char buf[10];
int fd,n,i,flags;
flags = fcntl(STDIN_FILENO,F_GETFL);//获取stdin属性信息
if(flags<0)
{
perror("fcntl error");
exit(1);
}
flags |= O_NONBLOCK;
int ret = fcntl(STDIN_FILENO,F_SETFL,flags);
if(ret<0)
{
perror("fcntl error");
exit(1);
}
for(i=0;i<5;i++)
{
n = read(fd,buf,10);
if(n>0)
{
break;
}
if(n<0)
{
if(errno != EAGAIN)
{
perror("read");
exit(1);
}
else
{
write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
sleep(2);
}
}
}
if(i == 5)
{
write(STDOUT_FILENO,"time out\n",strlen("time out\n"));
}
else
{
write(STDOUT_FILENO,buf,n);
}
close(fd);
return 0;
}
int flags = fcntl(fd,GETFL);
flags是F_SETFL的第三个参数
获取文件属性:F_GETFL。
设置文件属性:F_SETFL。
lseek函数
(1)文件指针:当我们对一个文件读写时,一定需要打开这个文件,所以我们操作的都是动态文件,动态文件在内存中的形态就是流的形式。
(2)文件流很长,里面有很多字节。那我们操作的是哪一个位置,GUI模式下就是用光标来标识,在动态文件中,我们通过文件指针来表示正在操作的位置。文件指针其实就是vnode里的一个元素。这个指针表示我们操作的位置,在linux中用lseek来访问这个指针。
(3)write和read函数本身自带移动文件指针的功能,所以打开一个有n字节的文件,read/write会自动从n字节后读写,如果要人为改变这个文件指针就用lssek函数。
off_t lseek(int fd, off_t offset, int whence);
fd:文件描述符
offset:偏移字节数
whence:文件指针所在位置(SEEK_SET:起始位置、SEEK_CUR:当前位置、SEEK_END:末尾地址)
返回值:返回偏移量
接下来我们来举例说明:
用lseek计算文件的大小
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc ,char *argv[])
{
int fd = -1;
int ret = -1;
if(argc != 2)
{
printf("usage: %s filename\n",argv[0]);
exit(-1);
}
fd = open(argv[1],O_RDONLY);
if(-1 == fd)
{
printf("打开文件出错\n");
exit(-1);
}
else
{
printf("打开成功,fd = %d\n",fd);
}
//此时文件指针指向文件开头,我们用lseek将文件指针移动到末尾
//然后返回值就是文件指针距离文件开头的偏移量,也就是文件的长度
ret = lseek(fd,0,SEEK_END);
printf("文件长度是%d个字节\n",ret);
return 0;
}
用lseek构建空洞文件
空洞文件就是这个文件有一段是空的
普通文件时不能为空的,因为我们write时文件指针是从前到后去移动的,不能绕过前面到后面
我们打开一个文件后,用lseek往后跳过一段,在write写入一段,就会构成一个空洞文件。
空洞文件对多线程共同操作文件是及其有用的,有时候我们创建一个很大的文件。如果从头依次构建是非常慢的,有一种思路将文件分为多段,然后多线程来操作每个线程负责其中一段写入。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int fd = -1;
int ret = -1;
char writeBuf[10] = "asdqwe";
fd = open("123.txt",O_RDWR|O_CREAT,0666);
if(fd == -1)
{
//printf("创建文件失败\n");
perror("创建失败");
_exit(-1);
}
else
{
printf("创建成功fd = %d\n",fd);
}
ret = lseek(fd,10,SEEK_SET);
int n = write(fd,writeBuf,strlen(writeBuf));
printf("写入了%d个字节\n",n);
printf("偏移了%d个字节\n",ret);
close(fd);
return 0;
}
turncate函数:截短文件
#include<unistd.h>
#include<sys/types.h>
int turncate(const char *path,off_t length);
//返回值:
成功:0
失败:-1(比如文件不存在)
//参数1:需要处理的文件
//参数2:off_t需要截短的字节数(文件的最终大小字节数)
1.如果截短字节数小于实际文件大小,那么文件被缩短了,多余的内容按丢弃处理。
2.如果截短字节数大于实际文件大小,那么文件就被拓展了,拓展的部分用'\0'填充。
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
int main()
{
if(truncate("demo.txt",20)!=-1)
{
printf("truncated!!!!\n");
}
return 0;
}
在命令行中输入:
echo "test demo.txt truncation,it's just test,so let's look is" > demo.txt
然后编译程序truncate.c并运行
使用命令stat demo.txt查看文件状态如下:
截长测试
更改上诉代码为:truncate("demo.txt",100000)
使用stat demo.txt查看文件状态如下:
虽然截长到 100000个字节,文件的长度是 100000字节,但是文件占用的 block 块只有 8 个。这多余部分就是一个空洞,并不占用实际的物理磁盘。
od显示文件或流
这对于访问或可视的检查文件中不能直接显示再终端上的字符很有用。
od -A x -tcx filename 查看文件:地址16进制,数据ASCII字符&&16进制
od -A d -tcd filename 查看文件:地址10进制,数据ASCII字符&&10进制
dup和dup2函数 重定向
在具体说dup/dup2之前,我认为有必要先了解一下文件描述符在内核中的形态。一个进程在此存在期间,会有一些文件被打开,从而会返回一些文件描述符,从shell中运行一个进程,默认会有3个文件描述符存在(0、1、2),0与进程的标准输入相关联,1与进程的标准输出相关联,2与进程的标准错误输出相关联,一个进程当前有哪些打开的文件描述符可以通过/proc/进程ID/fd目录查看。文件表中包含:文件状态标志、当前文件偏移量、v节点指针,这些不是本文讨论的重点,我们只需要知道每个打开的文件描述符(fd标志)在进程表中都有自己的文件表项,由文件指针指向。
dup和dup2函数
APUE和man文档都用一句话简明的说出了这两个函数的作用:复制一个现存的文件描述符。
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
当调用dup函数时,内核在进程中创建一个新的文件描述符,此描述符是当前可用文件描述符的最小数值,这个文件描述符指向oldfd所拥有的文件表项。
dup2和dup的区别就是可以用newfd参数指定新描述符的数值,如果newfd已经打开,则先将其关闭。如果newfd等于oldfd,则dup2返回newfd, 而不关闭它。dup2函数返回的新文件描述符同样与参数oldfd共享同一文件表项。
APUE用另外一个种方法说明了这个问题:
实际上,调用dup(oldfd)等效于,fcntl(oldfd, F_DUPFD, 0)
而调用dup2(oldfd, newfd)等效于,close(oldfd);fcntl(oldfd, F_DUPFD, newfd);
用一张图来说明就是:
dup函数:
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char* argv[])
{
int fd = open("hello", O_CREAT|O_RDWR|O_TRUNC, S_IRUSR|S_IWUSR);
if(fd < 0)
{
printf("Open Error!!\n");
return 0;
}
int nfd = dup(fd);
if(nfd < 0)
{
printf("Error!!\n");
return 0;
}
char buf[1000];
int n;
while((n = read(STDIN_FILENO, buf,1000)) > 0)
{
if(write(nfd, buf, n) != n)
{
printf("Write Error!!\n");
return 0;
}
}
return 0;
}
上面代码中,nfd 拷贝了 fd,所以 write ( nfd, buf, n ) 这语句写到 nfd 所代表的文件时也就是写到 fd 所代表的文件。程序执行完后可以在相应的目录的hello文件中看到输出。
dup2函数:
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char* argv[])
{
int fd = open("hello.file", O_CREAT|O_RDWR|O_TRUNC,S_IRUSR|S_IWUSR);
if(fd < 0)
{
printf("Open Error!!\n");
return 0;
}
int nfd = dup2(fd, STDOUT_FILENO);
if(nfd < 0)
{
printf("Error!!\n");
return 0;
}
char buf[5];
int n;
while((n = read(STDIN_FILENO, buf, 5)) > 0)
if(write(STDOUT_FILENO, buf, n) != n)
{
printf("Write Error!!\n");
return 0;
}
return 0;
}
上面的例子使用dup2将标准输出重定向为hello.file文件,如下所示:
以上就是今天的全部内容,文件这一块应该就结束了吧。