文件IO"/>
Linux学习之文件IO
引言
在学习Linux各种系统函数之前,我们需要先熟练掌握c语言。因为这些系统函数的用法必须结合Linux内核的工作原理来理解,而内核也是由c语言编写的,我们在描述内核工作原理时必然要用到"指针",“结构体”,“链表”这些名词来组织语言,所以在学习Linux系统函数之前,有良好的c语言基础才能更深一步了解到内核的工作原理。
一、C库IO函数的工作流程
首先来看一下C语言库中的fopen()函数是怎样执行的。
从图中可以看出来,使用fopen函数打开一个文件,会返回一个FILE* fp指针,这个指针指向的结构体有三个重要的成员:
-
文件描述符:通过文件描述符可以找到文件的inode,通多inode可以找到对应的数据块,简单点来说就是文件描述符可以找到文件在磁盘中的位置。
-
文件指针:操作文件时的一个指针,注意读操作和写操作共享一个文件指针,读操作或写操作都会引起文件指针的变化。也就是在文件中的位置。
-
文件缓冲区:读或者写都会先通过文件缓冲区,主要目的是为了减少对磁盘的操作次数,提高读写磁盘的效率。
数据从内存刷新到磁盘的方式:
1.刷新缓冲区函数:fflush()
2.当缓冲区满了会自动刷新
3.正常关闭文件,并且执行以下操作时会刷新:
1.使用fclose()
2.使用return(main函数中)
3.使用exit()(maina函数中)
在任何一个操作系统中,内存的读写速度比磁盘的读写速度快的很多。
缓冲区在IO操作中的作用:
注意:
- 在头文件stdio.h中有个FILE变量:typedef struct _IO_FILE_FILE FILE
- 在头文件libio.h就定义了这个结构体:struct _IO_FILE_FILE,而这个结构体中有一个__fileno成员,这就是文件描述符。
也就是说在对FILE* fp指针操作的时候,其实实际就是对文件描述符的操作。
二、C库函数与系统函数之间的关系
通过图我们可以看出,C库函数与系统函数之间的关系是:调用和被调用的关系;库函数是对系统函数进一步的封装。
C语言的printf()函数执行过程如下:
- printf()函数输出到标准输出stdout,而标准输出stdout也是一个文件,有对应的FILE*指针,也就是拥有文件描述符fd=1
- printf()函数内部调用操作系统提供的write()方法,将标准输出对应的文件描述符fd=1传递到应用层
- write()方法调用sys_write()系统函数,此时用户空间就转到内核空间
- sys_write()函数进一步调用设备驱动函数,进入到内核层
- 内核层通过设备驱动来操作硬件,进入硬件层,最终将数据打印到我们的显示器上。
**系统调用:**由操作系统实现并提供给外部应用程序的编程接口API(Application Programming Interface),是应用程序与系统之间数据交互的桥梁。
一个hello world如何打印到屏幕:
每一个FILE文件流(标准C库函数)都有一个缓冲区buffer,默认大小为8192Byte。Linux系统的IO函数默认是没有缓冲区的,要手动添加。
三、虚拟空间地址
Linux操作系统为每一个运行的程序(进程),都会为其分配一个0~4G的地址空间(虚拟地址空间)。
进程:正在运行的程序。程序在没启动之前是死的,启动之后可以被称为是一个进程。
如图,一个进程的虚拟地址空间可分为用户区和内核区,其中内核区是受保护的,用户是不能对该空间进行读写操作的。
在内核区,最重要的就是进程管理这一部分,进程管理中有一个区域叫做PCB(本质就是一个结构体),被称作进程管理控制块,PCB中有一个文件描述符表,文件描述符表中存放着当前进程中所有打开的文件描述符,涉及到文件的IO操作都会用到这些文件描述符表。
四、PCB和文件描述符表
根据这张图可以看出:
- 一个进程有一个文件描述符表,并且大小为1024。
- 在任何一个进程的文件描述符表中,前三个都是被占用的,对应的是标准输入(0->STDIN_FILENO),标准输出(1->STDOUT_FILENO)以及标准错误(2->STDERR_FILENO),并且默认都是打开状态,都最终操作在/dev/ttv当前终端上。终端也是一个文件,由此看出,Linux一切皆文件。
- 文件描述符的作用:通过文件描述符找到inode,通过inode找到磁盘数据块。
所以一个文件IO要操作文件描述符的顺序就是:
虚拟地址空间->内核区->PCB->文件描述符表->文件描述符->文件IO操作使用这个文件描述符。
五、IO函数
文件描述符: 一个进程启动之后,默认打开三个文件描述符:
#define STDIN_FILENO 0 //标准输入
#define STDOUT_FILENO 1 //标准输出
#define STDERR_FILENO 2 //标准错误
//注意:在使用标准文件描述符时尽量使用宏,不要写数字
新打开文件会返回当前进程中的文件描述符表中未使用的最小文件描述符。
调用open函数可以打开或新建一个文件,进而得到一个文件描述符。
open函数
-
函数描述:打开或新建一个文件
-
函数原型:
-
int open(const char *pathname, int flags);
-
int open(const char *pathname, int flags, mode_t mode);
注意:
1.函数参数类型为const char* 时,都代表是传入参数。如果是其他类型则有可能是传入参数,也有可能是传出参数。
2.一个类型是以 _t 结尾,大多都是使用 typedef 重定义的名称。
-
-
函数参数:
-
pathname:要打开或新建的文件名,和fopen函数一样,pathname既可以是相对路径,也可以是绝对路径。
-
flags:有一系列的常数值可供选择,可以同时选择多个常数,用按位 或 运算符连接起来,所以这些常数的宏定义都以 O_ 开头,代表 or 的意思。
-
必选项:文件打开方式,以下三个常数中必须指定一个,并且只能指定其中一个。
- O_RDONLY:以只读形式打开
- O_WRONLY:以只写方式打开
- O_RDWR:以可读可写方式打开
-
可选项:以下常数可以选择0个或者多个,和必选项要使用或( | )运算符连接起来作为flags参数。
可选项有很多,这里只介绍几个常用选项:
-
O_APPEND:表示追加。如果文件中已经有内容,那么这次打开后写的数据会追加到文件的末尾,而不会覆盖原来的文件内容。
-
O_CREAT:文件不存在则创建新文件。使用这个选项时需要提供第三个参数mode,表示该文件的访问权想。
文件最终的权限:mode & ~umask
umask:表示八进制掩码,即777->二进制:111 111 111
mode与umask取反进行与运算,就得到当前文件的权限。
-
O_EXCL:如果同时指定了O_CREAT,并且文件已存在,则出错返回。
-
O_TRUNC:如果文件已存在, 将其长度截断为为0字节。
-
O_NONBLOCK:对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O(Non blockI/O)。
-
-
-
-
函数返回值:
-
成功:返回一个最小且未被占用的文件描述符。
-
失败:返回-1,并设置errno值。
注意:
在Linux系统中,成功一般返回 0,失败返回 -1
-
close函数
- 函数描述:关闭一个文件
- 函数原型:int close(int fd);
- 函数参数:
- fd:文件描述符
- 函数返回值:
- 成功:返回 0。
- 失败:返回-1,并设置errno值。
注意:
当一个进程终止时,内核会对该进程所有尚未关闭的文件描述符调用close()函数关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭程序打开的所有文件。
但是对于一个常年累月运行的程序(比如说网络服务器),打开的文件描述符一定要记得关闭,否则随着打开文件的增多,会占用大量的文件描述符和系统资源,文件描述符表的大小是有限的。
因此,我们在打开并操作完文件之后,一定要记得使用close函数关闭。
read函数
- 函数描述:从打开的设备或文件中读取数据
- 函数原型:ssize_t read(int fd, void *buf, size_t count);
- 函数参数:
- fd:文件描述符
- buf:读到的数据保存在缓冲区buf中
- count:buf缓冲区存放的最大字节数
- 函数返回值:
- 返回值 > 0:代表读取到的字节数
- 返回值 = 0:代表文件读取完毕
- 返回值 = -1:读取失败,并设置errno
write函数
- 函数描述:向打开的设备或文件中写数据
- 函数原型:ssize_t write(int fd, const void *buf, size_t count);
- 函数参数:
- fd:文件描述符
- buf:缓冲区,要写入的文件或设备的数据
- count:buf中数据的长度
- 函数返回值:
- 成功:返回写入的字节数。
- 失败:返回-1,并设置errno值。
lseek函数
所有打开的文件都有一个当前文件偏移量(current file offset),当前文件偏移量通常是一个非负整数,用于表明文件开始处到文件当前位置的字节数,读写操作通常开始于当前文件偏移量,并且会是当前文件偏移量增大,增量为读写的字节数。文件被打开时,当前文件偏移量会被初始化为0,除非open函数使用了O_APPEND。
使用lseek函数可以灵活的改变当前文件偏移量的位置。
- 函数描述:移动文件指针
- 函数原型:off_t lseek(int fd, off_t offset, int whence);
- 函数参数:
- fd:文件描述符
- offset:取决于参数 whence
- whence 为 SEEK_SET,文件偏移量将设置为 offset
- whence 为 SEEK_CUR,文件偏移量将被设置为当前文件偏移量 + offset,此时offset可以为正也可以为负。
- whence 为 SEEK_END,文件偏移量将被设置为文件长度 + offset,同样offset可以为正也可以为负。
- whence:如上
- 函数返回值:
- 若lseek成功执行,则返回新的偏移量。
lseek 函数常用操作
//文件指针移动到文件头部
lseek(fd,0,SEEK_SET);
//获取文件指针当前位置
int len = lseek(fd,0,SEEK_CUR);
//获取文件长度
int len = lseek(fd,0,SEEK_END);//lseek实现文件扩展
off_t currpos;
//1.从文件尾部开始向后扩展1000个字节
currpos = lseek(fd,1000,SEEK_END);
//2.额外执行一次写操作,否则文件无法完成扩展
write(fd,"a",1);//数据随便写
IO函数测试
//IO函数测试--->open close read write lseek
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>int main(int argc, char *argv[])
{//打开文件int fd = open(argv[1], O_RDWR | O_CREAT, 0777);if(fd<0){perror("open error");return -1;}//写文件//ssize_t write(int fd, const void *buf, size_t count);write(fd, "hello world", strlen("hello world"));//移动文件指针到文件开始处//off_t lseek(int fd, off_t offset, int whence);lseek(fd, 0, SEEK_SET);//读文件//ssize_t read(int fd, void *buf, size_t count);char buf[1024];memset(buf, 0x00, sizeof(buf));int n = read(fd, buf, sizeof(buf));printf("n==[%d], buf==[%s]\n", n, buf);//关闭文件close(fd);return 0;
}
perror(C库函数)和errno
errno是一个全局变量,当系统调用后如果出现错误会自动将errno进行设置,perror可以将errno对应的错误信息打印出来;
如:perror(“open”);
如果报错的话会打印:open:(空格)errno对应得错误描述。
//测试perror函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>int main(int argc, char *argv[])
{//打开文件int fd = open(argv[1], O_RDWR);if(fd<0){perror("open error");if(errno==ENOENT){printf("same\n");}//return -1;}int n = 0;for(n=0; n<64; n++){errno = n;printf("[%d]:[%s]\n", n, strerror(errno));}//关闭文件//close(fd);return 0;
}
部分errno以及对应的错误信息
六、阻塞与非阻塞
思考:阻塞和非阻塞是文件的属性还是read函数的属性?
- 普通文件:helloc.c
- 默认是非阻塞的
- 终端设备:如 /dev/tty
- 默认阻塞
- 管道和套接字
- 默认阻塞的
练习:
1、测试普通文件是非阻塞的。
//验证read函数读普通文件是否阻塞
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>int main(int argc, char *argv[])
{//打开文件int fd = open(argv[1], O_RDWR);if(fd<0){perror("open error");return -1;}//读文件char buf[1024];memset(buf, 0x00, sizeof(buf));int n = read(fd, buf, sizeof(buf));printf("FIRST: n==[%d], buf==[%s]\n", n, buf);//再次读文件, 验证read函数是否阻塞memset(buf, 0x00, sizeof(buf));n = read(fd, buf, sizeof(buf));printf("SECOND: n==[%d], buf==[%s]\n", n, buf);//关闭文件close(fd);return 0;
}
2、测试终端设备文件 /dev/tty 是阻塞的。
//测试读设备文件是阻塞的
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main(int argc, char *argv[])
{//读文件char buf[64];memset(buf, 0x00, sizeof(buf));int n = read(STDIN_FILENO, buf, sizeof(buf));printf("read, n==[%d], buf==[%s]\n", n, buf);return 0;
}
通过读普通文件测试得知: read函数在读完文件内容之后, 若再次read,则read函数会立刻返回, 表明read函数读普通文件是非阻塞的.
设备文件: /dev/tty 标准输入STDIN_FILENO
通过读/dev/tty终端设备文件, 终端会等到待用户输入,表明read函数读设备文件是阻塞的.
结论: 阻塞和非阻塞是文件本身的属性,不是read函数的属性,并且socket pipe这两种文件都是阻塞的。
七、文件和目录
文件操作相关函数
stat/lstat函数
- 函数描述:获取文件属性
- 函数原型:
- int stat(const char *pathname, struct stat *buf);
- int lstat(const char *pathname, struct stat *buf);
- 函数返回值:
- 成功:返回0
- 失败:返回-1
stat结构体原型
struct stat {dev_t st_dev; //文件的设备编号ino_t st_ino; //节点mode_t st_mode; //文件的类型和存取的权限nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为1uid_t st_uid; //用户IDgid_t st_gid; //组IDdev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号off_t st_size; //文件字节数(文件大小)blksize_t st_blksize; //块大小(文件系统的I/O 缓冲区大小)blkcnt_t st_blocks; //块数time_t st_atime; //最后一次访问时间time_t st_mtime; //最后一次修改时间time_t st_ctime; //最后一次改变时间(指属性)};
文件类型和存取权限(8进制)
- st_mode -- 16位整数○ 0-2 bit -- 其他人权限S_IROTH 00004 读权限S_IWOTH 00002 写权限S_IXOTH 00001 执行权限S_IRWXO 00007 掩码, 过滤 st_mode中除其他人权限以外的信息○ 3-5 bit -- 所属组权限S_IRGRP 00040 读权限S_IWGRP 00020 写权限S_IXGRP 00010 执行权限S_IRWXG 00070 掩码, 过滤 st_mode中除所属组权限以外的信息○ 6-8 bit -- 文件所有者权限S_IRUSR 00400 读权限S_IWUSR 00200 写权限S_IXUSR 00100 执行权限S_IRWXU 00700 掩码, 过滤 st_mode中除文件所有者权限以外的信息If (st_mode & S_IRUSR) -----为真表明可读If (st_mode & S_IWUSR) ------为真表明可写If (st_mode & S_IXUSR) ------为真表明可执行○ 12-15 bit -- 文件类型S_IFSOCK 0140000 套接字S_IFLNK 0120000 符号链接(软链接)S_IFREG 0100000 普通文件S_IFBLK 0060000 块设备S_IFDIR 0040000 目录S_IFCHR 0020000 字符设备S_IFIFO 0010000 管道S_IFMT 0170000 掩码,过滤 st_mode中除文件类型以外的信息If ((st_mode & S_IFMT)==S_IFREG) ----为真普通文件if(S_ISREG(st_mode)) ------为真表示普通文件if(S_ISDIR(st.st_mode)) ------为真表示目录文件
stat函数测试->获取文件大小,文件属主和组
//stat函数测试: 获取文件大小, 文件属主和组
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>int main(int argc, char *argv[])
{//int stat(const char *pathname, struct stat *buf);struct stat st;stat(argv[1], &st);printf("size:[%d], uid:[%d], gid:[%d]\n", st.st_size, st.st_uid, st.st_gid);return 0;
}
stat函数测试->获取文件类型和权限
//stat函数测试: 获取文件类型和权限
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>int main(int argc, char *argv[])
{//int stat(const char *pathname, struct stat *buf);//获取文件属性struct stat sb;stat(argv[1], &sb);//获取文件类型if ((sb.st_mode & S_IFMT) == S_IFREG) {printf("普通文件\n");} else if((sb.st_mode & S_IFMT) ==S_IFDIR){printf("目录文件\n");}else if((sb.st_mode & S_IFMT) ==S_IFLNK){printf("连接文件\n");}if (S_ISREG(sb.st_mode)) {printf("普通文件\n");}else if(S_ISDIR(sb.st_mode)){printf("目录文件\n");}else if(S_ISLNK(sb.st_mode)){printf("连接文件\n");}//判断文件权限if(sb.st_mode & S_IROTH){printf("---R----");}if(sb.st_mode & S_IWOTH){printf("---W----");}if(sb.st_mode & S_IXOTH){printf("---X----");}printf("\n");return 0;
}
lstat函数测试
先创建链接文件 ln -s b a (软连接) a->b
//stat函数测试: 获取文件大小, 文件属主和组
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>int main(int argc, char *argv[])
{//int stat(const char *pathname, struct stat *buf);struct stat st;lstat(argv[1], &st);printf("size:[%d], uid:[%d], gid:[%d]\n", st.st_size, st.st_uid, st.st_gid);return 0;
}
如图,c和d分别是 lstat 函数和 stat 函数测试链接文件a的属性。
lstat函数 获取的是链接文件 a 本身的属性信息,而 stat 函数获取的是链接文件 a 指向的 b 文件为属性信息。
stat函数和lstat函数的区别:
- 对于普通文件,这两个函数没有区别,是一样的。
- 对于链接文件,调用 lstat 函数获取的是链接文件本身的属性信息。而 stat 函数获取的是链接文件指向的文件属性信息。
目录操作相关函数
opendir函数
- 函数描述:打开一个目录
- 函数原型:DIR *opendir(const char *name);
- 函数参数:要遍历的目录(相对路径或绝对路径都可以)
- 函数返回值:指向目录的指针
readdir函数
- 函数描述:读取目录内容—目录项
- 函数原型:struct dirent *readdir(DIR *dirp);
- 函数参数:opendir 函数的返回值,目录指针
- 函数返回值:读取的目录指针
struct dirent结构体原型
struct dirent
{ino_t d_ino; // 此目录进入点的inodeoff_t d_off; // 目录文件开头至此目录进入点的位移signed short int d_reclen; // d_name 的长度, 不包含NULL 字符unsigned char d_type; // d_name 所指的文件类型 char d_name[256]; // 文件名
};
d_type的取值:
d_type的取值: DT_BLK - 块设备DT_CHR - 字符设备DT_DIR - 目录DT_LNK - 软连接DT_FIFO - 管道DT_REG - 普通文件DT_SOCK - 套接字DT_UNKNOWN - 未知
目录结构图
closedir函数
- 函数描述:关闭目录
- 函数原型:int closedir(DIR *dirp);
- 函数参数:opendir函数返回的目录指针
- 函数返回值:
- 成功:返回 0
- 失败:返回 -1
读取目录内容的一般步骤
//1.打开目录
DIR *pDir = opendir("dir");
//2.循环读取文件--目录项
while ((p = readdir(pDir) != NULL)){ };
//3.关闭目录
closedir(pDir);
递归遍历目录下所有的文件, 并判断文件类型:
//目录操作测试: opendir readdir closedir
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <dirent.h>int main(int argc, char *argv[])
{//打开目录//DIR *opendir(const char *name);DIR *pDir = opendir(argv[1]);if(pDir==NULL){perror("opendir error");return -1;}//循环读取目录项//struct dirent *readdir(DIR *dirp);struct dirent *pDent = NULL;while((pDent=readdir(pDir))!=NULL){//过滤掉.和..文件if(strcmp(pDent->d_name, ".")==0 || strcmp(pDent->d_name, "..")==0){continue;}printf("[%s]---->", pDent->d_name);//判断文件类型switch(pDent->d_type){case DT_REG:printf("普通文件");break;case DT_DIR:printf("目录文件");break;case DT_LNK:printf("链接文件");break;default:printf("未知文件");}printf("\n");}//关闭目录closedir(pDir);return 0;
}
注意:
递归遍历指定目录下的所有文件的时候, 要过滤掉 . 和 … 文件, 否则会进入死循环
八、dup/dup2/fcntl函数
dup函数
- 函数描述:复制文件描述符
- 函数原型:int dup(int oldfd);
- 函数参数:
- oldfd:要复制的文件描述符
- 函数返回值:
- 成功:返回在文件描述符表里的最小且没被占用的文件描述符
- 失败:返回-1,设置errno值
dup函数测试:
//测试dup函数复制文件描述符
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main(int argc, char *argv[])
{//打开文件int fd = open(argv[1], O_RDWR);if(fd<0){perror("open error");return -1;}//调用dup函数复制fdint newfd = dup(fd);printf("newfd:[%d], fd:[%d]\n", newfd, fd);//使用fd对文件进行写操作write(fd, "hello world", strlen("hello world"));//调用lseek函数移动文件指针到开始处lseek(fd, 0, SEEK_SET);//使用newfd读文件char buf[64];memset(buf, 0x00, sizeof(buf));int n = read(newfd, buf, sizeof(buf));printf("read over: n==[%d], buf==[%s]\n", n, buf);//关闭文件close(fd);close(newfd);return 0;
}
dup2函数
- 函数描述:复制文件描述符
- 函数原型:int dup2(int oldfd, int newfd);
- 函数参数:
- oldfd:原来的文件描述符
- newfd:复制的新的文件描述符
- 函数返回值:
- 成功:将oldfd复制给newfd,两个文件描述符指向同一个文件。
- 失败:返回-1,并设置errno
注意:
1、假如newfd已经指向了一个文件,需要先 close 原来的文件,然后 newfd 再指向 oldfd 指向的文件。
2、如果newfd没有被占用,那么 newfd 直接指向oldfd指向的文件。
使用dup2函数实现文件描述符复制:
//测试dup2函数复制文件描述符
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main(int argc, char *argv[])
{//打开文件int oldfd = open(argv[1], O_RDWR | O_CREAT, 0755);if(oldfd<0){perror("open error");return -1;}int newfd = open(argv[2], O_RDWR | O_CREAT, 0755);if(newfd<0){perror("open error");return -1;}//调用dup2函数复制fddup2(oldfd, newfd);printf("newfd:[%d], oldfd:[%d]\n", newfd, oldfd);//使用fd对文件进行写操作write(newfd, "hello world", strlen("hello world"));//调用lseek函数移动文件指针到开始处lseek(newfd, 0, SEEK_SET);//使用newfd读文件char buf[64];memset(buf, 0x00, sizeof(buf));int n = read(oldfd, buf, sizeof(buf));printf("read over: n==[%d], buf==[%s]\n", n, buf);//关闭文件close(oldfd);close(newfd);return 0;
}
使用dup2函数将终端输出重定向到文件中
//使用dup2函数实现标准输出重定向操作
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main(int argc, char *argv[])
{//打开文件int fd = open(argv[1], O_RDWR | O_CREAT, 0777);if(fd<0){perror("open error");return -1;}//调用dup2函数实现文件重定向操作//标准输入文件描述符此时指向fd指向的文件dup2(fd, STDOUT_FILENO);printf("ni hao hello world");close(fd);close(STDOUT_FILENO);return 0;
}
dup/dup2函数原理图
dup函数原理图
当调用dup函数之后,newfd和fd都指向了同一个文件test.txt,内核会在 内部维护一个计数,此时计数为2,当close一个文件描述符后,这个计数变为1,只有当计数为0时,文件才会真正被关闭。
通过fd对文件进行写操作,使用newfd对文件进行读操作,如果读到了文件内容,则认为fd和newfd指向相同的文件。
dup2函数原理图
当调用dup2(oldfd,newfd)后:
- 如果newfd之前已经打开了一个文件,则先关闭这个文件,然后newfd指向了和oldfd相同的文件。
- 如果newfd之前没有打开文件,则newfd直接指向和oldfd相同的文件。
- 调用dup2函数之后,内核会修改内部的计数,计数为2
验证思路:
1、打开两个文件,得到oldfd和newfd两个文件描述符
2、调用dup2(oldfd,newfd)
3、使用newfd写文件
4、使用oldfd读文件
5、如果oldfd读到了文件内容,则认为newfd和oldfd指向了相同的文件
dup2实现文件重定向原理图
fcntl函数
- 函数描述:改变已经打开的文件的属性
- 函数原型:int fcntl(int fd, int cmd, … /* arg */); // … 代表可变参数
- 函数参数:
- 如果cmd为 F_DUPFD,复制文件描述符,与dup相同
- 如果cmd为 F_GETFL,获取文件描述符的 flag 属性值
- 如果cmd为 F_SETFL,设置文件描述符的 flag 属性
- 函数返回值:
- 取决于cmd
- 成功:
- 如果cmd为 F_DUPFD,返回一个新的文件描述符
- 如果cmd为 F_GETFL,返回文件描述符的 flags 值
- 如果cmd为 F_SETFL,设置文件描述符的 flags 值
- 失败:返回 -1,并设置errno
- 成功:
- 取决于cmd
fcntl函数常用操作:
//1.复制一个新的文件描述符
int newfd = fcntl(fd,F_DUPFD,0);
//2.获取文件的属性标志
int flag = fcntl(fd,F_GETFL,0);
//3.设置文件标志
flag |= O_APPEND;
fcntl(fd,F_SETFL,flag);
//4.常用的属性标志
O_APPEND //设置文件打开为末尾添加
O_NONBLOCK //设置打开的文件描述符为非阻塞
使用fcntl函数在文件的末尾添加内容
//修改文件描述符的flag属性
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>int main(int argc, char *argv[])
{//打开文件int fd = open(argv[1], O_RDWR);if(fd<0){perror("open error");return -1;}//获得和设置fd的flags属性int flags = fcntl(fd, F_GETFL, 0);flags = flags | O_APPEND;fcntl(fd, F_SETFL, flags);//写文件write(fd, "hello world", strlen("hello world"));//关闭文件close(fd);return 0;
}
总结:
fcntl函数:
1、复制文件描述符: int fd = fcntl(oldfd, F_DUPFD, 0);
2、获得和设置文件的flag属性:
int flag = fcntl(fd, F_GETFL, 0);
flag |= O_APPEND;
fcntl(fd, F_SETFL, flag);
更多推荐
Linux学习之文件IO
发布评论