基于文件描述符的文件操作
进程一启动,内核就打开了三个描述符,0(标准输入 STDIN),1(标准输出STDOUT), 2(标准错误输出STDERR)。Linux用整形数做文件操作,因此称为文件描述符,文件描述符是一个较小的整数(0~1023)。
内核为进程维护一个已打开文件的记录表,文件描述符代表记录表里的一项,通过描述符和操作函数,即可实现文件操作。
常用基于文件描述符的函数有:open(打开)、creat(创建)、close (关闭)、read(读取)、write(写入)、ftruncate(改变文件大小)、lseek(定位)、fsync (同步)、fstat(获取文件状态)、fchmod(权限)、flock(加锁)、fcntl(控制文件属性)、 dup(复制)、dup2、select
常用函数
- 打开文件
int open(const char *pathname, int flags); //文件名 打开方式
int open(const char *pathname, int flags, mode_t mode); //文件名,打开方式,权限
flags和Mode都是一组掩码的合成纸,flag表示打开方式,mode表示访问权限
以下为flags:
掩码 | 作用 |
---|---|
O_RDONLY | 以只读的方式打开 |
O_WRONLY | 以只写的方式打开 |
O_RDWR | 以读写的方式打开 |
O_CREAT | 若文件不存在则创建文件 |
O_EXCL | 若文件已存在则创建失败返回已存在 |
O_TRUNC | 若文件存在,将长度截为0 |
O_APPEND | 用追加的方式打开文件,每次调用write时,文件指针自动移到文件尾。(一般用于多进程写同一个文件) |
O_NONBLOCK | 以非阻塞的方式打开,无论有没有数据读取或者等待,都会立刻返回进程。 |
O_SYNC | 同步打开文件,当数据被真正写入**物理设备(磁盘)**后才返回 |
- 读写文件
ssize_t read(int fd, void *buf, size_t count); //文件描述符,缓冲区,长度
ssize_t write(int fd, const void *buf, size_t count); //同上
以下为文件指针和文件描述符转换的两个接口:
int fileno(FILE *stream); //将文件指针FILE *fp转换成文件描述符fd
FILE *fdopen(int fd, const char *mode); //将文件描述符fd转换成文件指针fp
#include <func.h>
int main(int argc,char *argv[])
{
FILE *fp=fopen(argv[1],"rb+");
if(NULL==fp)
{
perror("fopen");
return -1;
}
int fd=fileno(fp);//将文件指针转换为文件描述符fd
printf("fd=%d\n",fd);
int arr[3]={1,2,3};
//char buf[128]="hello";
//write(3,buf,strlen(buf));
write(fd,arr,sizeof(arr));
//close(fp);
return 0;
}
- 改变文件大小
int ftruncate(int fd, off_t length);
函数 ftruncate 会将参数 fd 指定的文件大小改为参数 length 指定的大小。参数 fd 为已打开的文件描述词,而且必须是以写入模式打开的文件。如果原来的文件大小比参数 length大,则超过的部分会被删去。函数执行成功则返回 0,失败返回-1。
#include <func.h>
void change(int fd)//可以传入fd而不用传入地址,因为fd描述的是一个打开的文件对>
{
ftruncate(fd,1);//将文件大小改为1个字节
}
int main(int argc,char *argv[])
{
int fd=open(argv[1],O_RDWR);
if(-1==fd)
{
perror("open");
return -1;
}
ftruncate(fd,4);//将文件大小改成4个字节
change(fd);
close(fd);
return 0;
}
- 文件定位
off_t lseek(int fd, off_t offset, int whence); //fd为文件描述符
whence可以是以下三个值:
SEEK_SET 从文件头开始计算
SEEK_CUR 从当前指针开始计算
SEEK_END 从文件尾开始计算
注:文件空洞:假设file中写入hello,此时用 lseek(fd,1000,SEEK_SET) 则会偏移1000个字节,
hello与末尾之间的“0”称之为文件空洞。
#include <func.h>
int main(int argc,char *argv[])
{
ARGS_CHECK(argc,2);
int fd=open(argv[1],O_RDWR);
if(-1==fd)
{
perror("open");
return -1;
}
int ret;
ret=lseek(fd,1000,SEEK_SET);//文件空洞
printf("ret=%d\n",ret);
write(fd,"1",1);
close(fd);
return 0;
}
-
获取文件信息
int fstat(int fd, struct stat *buf); //文件描述符 stat结构体指针
stat结构体 -
struct stat { dev_t st_dev; /* ID of device containing file */ //设备,返回设备描述符,没有设备则返回0 ino_t st_ino; /* inode number */ //文件inode信息 mode_t st_mode; /* protection */ //文件类型 nlink_t st_nlink; /* number of hard links */ //链接数目 uid_t st_uid; /* user ID of owner */ //使用者ID gid_t st_gid; /* group ID of owner */ //组ID dev_t st_rdev; /* device ID (if special file) */ //设备类型 off_t st_size; /* total size, in bytes */ //文件大小,以字节为单位表示 blksize_t st_blksize; /* blocksize for filesystem I/O */ //块大小,LINUX下每一块为512B blkcnt_t st_blocks; /* number of 512B blocks allocated */ //块数 time_t st_atime; /* time of last access */ //最后访问时间 time_t st_mtime; /* time of last modification */ //最后修改时间 time_t st_ctime; /* time of last status change */ //最后权限修改时间 };
-
文件描述符的复制
系统调用函数dup和dup2可以实现米文件描述符的复制,常用于重定向进程的STDIN(0),STDOUT(1),STDERR(2)
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#include <func.h>
int main(int argc,char *argv[])
{
ARGS_CHECK(argc,2);
int fd=open(argv[1],O_RDWR);//此时fd在内核结构体指针数组为3
if(-1==fd)
{
perror("open");
return -1;
}
int fd1=dup(fd);//成功例子,此时fd1在内核结构体指针数组为4
//int fd1=fd;//失败例子,fd无法直接赋值给fd1,需要dup
printf("fd1=%d\n",fd1);//for test fd1在内核结构体指针数组下标为4,因此打印出来为4
close(fd);
char buf[128]={0};
int ret=read(fd1,buf,sizeof(buf));
if(-1==ret)
{
perror("read");
return -1;
}
printf("ret=%d,buf=%s\n",ret,buf);
return 0;
}
失败例子的原理:
int fd1=fd;
close(fd);
read(fd,buf,sizeof(buf);
perror(“read”);
一旦close(fd)后,3的结构体指针被free,fd1无法指向文件对象,因此执行后显示bad file descriptor
内核原理如下图所示:
dup不同,dup会在结构体指针数组中复制出3号的复制品4号fd1,指向相同的文件对象( int fd1=dup(fd) ),具体内核原理如下图:
因此可知,文件描述符的复制是指用另外一个文件描述符指向同一个打开的文件,它完全不同于直接给文件描述符变量赋值。两个描述符( fd和fd1 )共享同一个数据结构,需注意,dup 返回新的文件描述符是没有使用的文件描述符的最小编号。
由于dup 返回新的文件描述符是没有使用的文件描述符的最小编号,则可以进行重定向标准输出
假设将标准输出1关闭(close(1)),再使用 dup ,则 fd1 的值为1;
#include <func.h>
// 重定向标准输出
int main(int argc,char *argv[])
{
ARGS_CHECK(argc,2);
int fd=open(argv[1],O_RDWR);
if(-1==fd)
{
perror("open");
return -1;
}
printf("\n");//刷新标准输入缓冲区
close(1);//关闭标准输入缓冲区
int fd1=dup(fd);//此时fd1的值则为1(没有使用的文件描述符的最小编号为1)
printf("fd1=%d\n",fd1);//由于标准输入缓冲区关闭了,printf会写到文件中
close(fd);
printf("I am mark2 \n");//测试是否会写到文件里
return 0;
}
dup会自动去找没有使用的文件描述符的最小编号,当要指定一个文件描述符的位置复制时,则可以使用dup2(fd,1) //则会复制 fd 并将 fd1 的文件描述符设置为1,具体代码如下:
#include <func.h>
// 重定向标准输出
int main(int argc,char *argv[])
{
ARGS_CHECK(argc,2);
int fd=open(argv[1],O_RDWR);
if(-1==fd)
{
perror("open");
return -1;
}
printf("\n");//刷新标准输入缓冲区
int fd1=dup2(fd,1);//指定1为fd1的文件描述符编号,dup2会自动close(1)
printf("fd1=%d\n",fd1);//由于标准输入缓冲区关闭了,printf会写到文件中
close(fd);
printf("you cant see me\n");//测试是否会写到文件里
return 0;
}
- 内存映射文件(mmap)
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
mmap将一个文件(以页为单位),映射进内存。降低cpu对文件读取的干预,提高性能。
使用场景:当存在多进程或者多线程对同一个文件进行操作——mmap
mmap参数:
star:一般填写NULL意为由操作系统帮助我们寻找一块空闲的地址
length:文件长度,采用缺页异常设计
prot:内存权限,一般只用PROT_READ(页内容可读),PROT_WRITE(页内容可写),采用按位或的操作。
flags:MAP_SHARED 与其他所有映射此对象的进程共享映射空间(即多进程共享一个文件)
fd:文件描述符
offset:只能是4K的整数倍。 一般不偏移,填写0
代码如下:
#include <func.h>
int main(int argc,char *argv[])
{
ARGS_CHECK(argc,2);//参数设置
int fd=open(argv[1],O_RDWR);//以读写的方式打开文件描述符
if(-1==fd)
{
perror("open");
return -1;
}
char *p;
struct stat buf;//定义一个文件状态结构体
fstat(fd,&buf);//获取文件大小
p=mmap(NULL,buf.st_size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if((char *)-1==p)
{
perror("mmap");
return -1;
}
strcpy(p,"HELLOWORLD");
munmap(p,buf.st_size);//回写到磁盘中
close(fd);
return 0;
}
int munmap(void *addr, size_t length); //写回磁盘的接口
int msync(void *addr, size_t length, int flags); //同步写入磁盘的接口
Q:mmap和read,write有什么区别,为什么要使用mmap?
A:read,write会进行数据的多(两)次拷贝,而mmap为零拷贝。read和write要进行多次的数据搬移,而mmap不需要,因此性能较read和write较好。
以下为两次数据搬移(即两次拷贝)
以下为mmap原理图: