文件IO就是直接调用内核提供的系统调用函数。
标准IO就是间接调用系统调用函数,是C库函数。
文件IO是直接调用内核提供的系统调用函数,头文件是unistd.h,标准IO是间接调用系统调用函数,头文件是stdio.h,文件IO是依赖于Linux操作系统的,标准IO是不依赖操作系统的,所以在任何操作系统下,使用标准IO,也就是C库函数操作文件的方法都是相同的。
文件描述符(file descriptor)通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件; 当内核打开一个现有文件或创建一个新文件时,它都返回一个文件描述符。 在读、写文件时,可以使用这个文件描述符。
当我们对一个文件做读写操作的时候,我们是用open函数返回的这个文件描述符会标识该文件,并将其作为参数传递给read或者write函数。
标准输入、标准输出和标准出错
按惯例,每当运行一个新程序时,所有的shell都为其打开3个文件描述符,即标准输入(stdin)、标准输出(stdout)以及标准出错(stderr) 分别对应0, 1, 2 ;对于这些描述符,可以通过重定向例如标准输出,将其重定向到一个文件中去,那么输出的内容便不会在终端显示,而是写入到文件中去。
对于标准输入/输出/出错 的符号常量
STDIN_FILENO, STDOUT_FILENO 和 STDERR_FILENO
如图 显示了 一个进程对应的3张表之间的关系。 该进程有两个不同的打开文件:0 stdin 1 stdout
如果两个独立进程各自打开了同一个文件,则有如图所示的关系
每个进程在进程表中都有一个记录项,进程表项中包含有一张打开文件描述符表
(1)记录了文件的文件描述符标志
(2)指向一个文件表项的指针
文件表项包含:
(1)文件状态标志(读、写、填写、同步和非阻塞等)
(2)当前文件偏移量
(3)指向v节点的指针
v节点包含了文件类型和对此文件进行各种操作的函数的指针
对于大多数文件,v节点还包含了该文件的i节点,i节点包含了文件的所有者、文件长度、文件所在设备、指向文件实际数据块在磁盘上所在位置的指针等
(linux系统只使用i节点,而不使用v节点)
追加到一个文件 O_APPEND是一个原子操作;
例如A、B两个进程同时对文件进行写入,如上图两个进程都有一个文件表项,但是共享V节点表项。
假定进程A调用了lseek,它将进程A的该文件偏移量设置为1500字节(当前文件尾端处)。然后内核切换进程,进程B运行。进程B也使用lseek定位到1500字节(当前文件尾端处),由于进程调度,A写入文件100字节,将A的文件表项中的当前文件偏移量更新至1600,此时将V节点的当前文件长度更新至1600。进程调度B运行执行与A相同的操作,这便会导致将A进程写入的数据给覆盖掉。
打开文件时设置O_APPEND标志。 这样做使得内核在每次写操作之前,都将进程的当前文件偏移量设置到该文件的尾端处,于是在每次写之前就不再需要调用lseek。
char *str1 = "hello world";
char * str ="11122222";
source_fd = open(argv[1], O_RDWR | O_APPEND); //以O_APPEND标志打开
write(source_fd, str1, strlen(str1)); //写入数据
lseek(source_fd, 0, SEEK_SET); //将文件偏移量定位到文件开始
read(source_fd, buf, 5); //读取5个字节,此时文件偏移量为5字节
write(source_fd, str, strlen(str)); //再次写入数据
lseek(source_fd, 0, SEEK_SET);
read(source_fd, buf, 512); //读取
这里可以看到,使用O_APPEDN之后再次写入直接从文件的末尾开始写入,而不是从当前文件偏移量的位置写入
函数原型
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 文件路径
flags 对打开的文件操作的选项 当使用多个选项是使用 “或”操作 例如 O_CREAT | O_RDWR
mode 创建文件时的权限,会与~umask进行与操作 (mode和umask异或操作)例如默认情况下umask为022 当我们创建一个文件传递的权限为0777时,那么实际的权限为~022 & 0777 = 0755 也就是所谓的屏蔽掉了相应的权限
关于mode参数, 需要使用O_CREAT才能使其生效
r – 4 w-- 2 x–1
flags | 功能 |
---|---|
O_WRONLY | 只写打开 |
O_RDONLY | 只读打开 |
O_RDWR | 读写打开 |
O_EXEC | 只执行打开 |
O_APPEND | 每次写时都追加到文件的末尾(默认打开是从文件开始位置处写入) 当以write操作时,都会自动将文件偏移量自动定位到文件的末尾 |
O_CREAT | 如果不存在则创建,使用此选项,需要使用mode参数,但是如果想要得到需要的权限需要对umask进行修改 |
O_EXCL | 如果同时指定了O_CREAT,而文件已存在,则出错;可以用来测试该文件是否存在 |
O_NOCTTY | 如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端,如串口设备 |
O_TRUNC | 如果此文件存在,并且为 只写 或 读写方式打开,则将其长度截断为0 |
O_NONBLOCK或O_NDELAY | 此选项为文件的本次打开操作和后续的I/O操作设置为 非阻塞方式, 不使用该参数默认为阻塞模式,最好使用O_NONBLOCK选项而不是O_NDELAY选项,因为后者的读操作返回值具有二义性,例不能从管道、FIFO或设备中读得数据,则返回0 ,而与读到文件尾返回0 冲突 |
返回值 成功 0 失败返回 -1
关闭一个打开的文件
#include
int close(int fd);
关闭一个文件时还会释放该进程加在该文件上的所有记录锁。
当一个进程终止时,内核自动关闭它所有的打开文件。 很多程序都利用了这一点而不显示地调用close关闭打开的文件。
#include
ssize_t read(int fd, void *buf, size_t count); // ssize_t 实际上就是int 带符号的整型数字
如read成功返回,则返回读到的字节数,如已到达文件的尾端,则返回0 , 失败则 返回 - 1
有多种情况可实际读到的字节数少于要求读的字节数。
(1)读普通文件时,在读取到要求字节数之前已经到达了文件尾端; 例如 文件尾端之前只有30个字节,而要求读取100个字节,那么read返回30,下一次再次调用时返回0
(2) 当从终端设备读取时,通常一次最多读一行。
(3) 当从网络读取时,网络中的缓冲机制可能造成返回值小于所要求的字节数。
(4) 当从管道或FIFO读取时,若管道包含的字节少于所需的字节数,那么返回实际可用的字节数
(5) 当一信号造成中断,而已经读了部分数据量时。 读操作从文件的当前偏移量初开始,在成功返回钱,该偏移量将增加实际读到的字节数
工程中的read函数写法
do{
ret = read(fd, buff, BUF_SIZE);
}while(ret == -1 && errno == EINTR);
if (-1 == ret)
{
perror("read");
return -1;
}
#include
ssize_t write(int fd, const void *buf, size_t count);
向打开的文件写入数据。
成功返回 实际写入的字节数, 失败返回 -1
其返回值通常与参数count的值相同,否则表示出错。 write出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制。
例如 完成cp命令
#include
#include
#include
#include
int main(int argc,char *argv[])
{
if (3 != argc)
{
printf("无效的参数\n");
printf("./cp file_a file_b\n");
}
int source_fd, target_fd;
char buf[1024] = {'\0'};
int ret = 1;
source_fd = open(argv[1], O_RDONLY);
if (source_fd == -1){
perror("open");
return -1;
}
target_fd = open(argv[2], O_CREAT | O_RDWR, 0777);
if (target_fd == -1)
{
perror("open");
return -1;
}
while(ret)
{
do{
ret = read(source_fd, buf, sizeof(buf));
}while(ret == -1 && errno == EINTR);
if (ret == -1)
{
perror("read");
return -1;
}
ret = write(target_fd, buf, ret);
if (ret == -1)
{
perror("write");
return -1;
}
}
return 0;
}
每当打开文件都有一个与其相关的“当前文件偏移量”(current file offset)。 它通常是一个非负整数,用以度量从文件开始出计算的字节数。 通常,读、写操作都是从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置0
#include
#include
off_t lseek(int fd, off_t offset, int whence);
对于offset的解释与参数whence的值有关
whence | offset |
---|---|
SEEK_SET | 将该文件偏移量设置为距文件开始处的offset个字节 只能为正数 |
SEEK_CUR | 将该文件的偏移量设置为其当前值加offset, offset可正可负 |
SEEK_END | 将改文件偏移量设置为文件长度加offset, offset可正可负 |
示例:
lseek(fd , 0 , SEEK_SET); //文件开始位置
lseek(fd , 0 , SEEK_END); //文件末尾位置
lseek(fd , 10 , SEEK_END); //从文件末尾往后移动10个字节
lseek(fd , -10 , SEEK_END); //从文件末尾往前移动10个字节
lseek(fd , 100 , SEEK_SET); //从文件开始往后移动100个字节
注意:
如果文件偏移量往回超出文件头位置,则返回-1,文件指针不变,还是处于原来的位置。
lseek并不适用与所有的文件类型,也就是说在管道,FIFO,socket或终端不能使用lseek函数,一旦调用将会失败,并设置errno为EPIPE。
通常,文件的当前偏移量应当是一个非负整数,但是,某些设备 也可能允许负的偏移量; 对于普通文件,其偏移量必须是非负值。因为某些设备的偏移量可能是负值,所以在比较lseek的返回值时应当谨慎,不要测试是否小于0,而要测试是否等于-1
4. lseek仅将当前的文件偏移量记录在内核中,它并不引起任何的I/O操作。 然后,该偏移量用于下一个读或写操作。
5. 文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都将被读为0
```
char * str ="11122222";
ret = lseek(source_fd, 30, SEEK_END);
if (ret == -1)
{
perror("lseek");
return -1;
}
write(source_fd, str, strlen(str));
```
有时我们需要在文件尾端处截去一些数据以缩短文件。将一个文件的长度截断为0是一个特例,在打开文件时使用O_TRUNC标志可以做到这一点。为了截断文件可以调用函数truncate() 和 ftruncate()
#include
#include
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
这两个函数将一个现有文件长度截断为length。 如果该文件以前的长度大于length,则超过length以外的数据就不能访问。如果以前的长度小于length,文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作0(也就是可能在文件中创建了一个空洞)。
但是,如果说之前文件中有数据,但是当重新写入的数据小于原有数据时,那么便会发生此时的数据后面包含未被覆盖的数据
例如连续之前写入了hello worldhello world hello worldhello world;
当此时写入hello worldhello world 再次读取时,便会将未覆盖的数据也会输出
解决办法1、 首先可以使用O_TRUNC 参数,但是,当以只写或读写方式打开的时候,便清空数据
那么再读取原有数据时,便会无数据可读
解决办法2
使用ftruncate函数
ftruncate(fd, length);
函数原型
#include
int dup(int oldfd);
int dup2(int oldfd, int newfd);
由dup返回的新文件描述符一定是当前可用文件描述符中的最小值。对于dup2,可以用newfd参数来指定新的文件描述符的值。如果newfd已经打开,则先将其关闭。若oldfd == newfd, 则dup2返回newfd,而不关闭它。
dup/dup2主要是用来进行文件描述符重定向,例如将 stdin或stdout 重定向到文件
例如将标准输出重定向到文件中
#include
#include
#include
int main(int argc,char *argv[])
{
int oldfd, newfd;
oldfd = open("stdout_file", O_CREAT | O_RDWR, 0777);
// close(STDOUT_FILENO); //关闭标准输出
// newfd = dup(oldfd); //将标准输出重定向到stdout_file
newfd = dup2(oldfd, STDOUT_FILENO); //将标准输出重定向到文件 先关闭标准输出
printf("hello world\n");
return 0;
}
#include
#include
int fcntl(int fd, int cmd, ... /* arg */ );
成功,返回值依赖于cmd 失败返回-1
fcntl函数有以下5种功能
(1) 复制一个已有的文件描述符(cmd = F_DUPFD 或者 F_DUPFD_CLOEXEC)
(2) 获取/设置文件描述符标志 (cmd = F_GETFD 或 F_SETFD)。
(3) 获取/设置文件状态标志(cmd = F_GETFL 或 F_SETOWN)。
(4) 获取/设置异步I/O所有权 (cmd = F_GETOWN 或 F_SETOWN)。
(5) 获取/设置记录锁 (cmd = F_GETLK、 F_SETLK 或 F_SETLKW)。
这里先说明复制文件描述符操作,后面的在学习相应操作时再补充
cmd | 功能 |
---|---|
F_DUPFD | 复制文件描述符fd。新文件描述符作为函数值返回。他是尚未打开的描述符中大于等于第3个参数值当前未用的最小的文件描述符。 但是,新的文件描述符有自己的一套文件描述符标志,其FD_CLOEXEC文件描述符标志会被清除 0 (这表示该描述符在exec时仍保持有效) |
F_DUPFD_CLOEXEC | 复制文件描述符,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新的文件描述符 与F_DUPFD的区别是 前者会将FD_CLOEXEC设置为0 而后者会复制和被复制的文件描述符相同的FD_CLOEXEC值 |
F_GETFD | 对应于fd的_文件描述符标志_作为**函数值返回****。当前只定义了一个文件描述符标志FD_CLOEXEC 默认值为0 |
F_SETFD | 对于fd设置文件描述符标志。新标志值按第3个参数(取为整数值)设置 |
关于 执行时关闭close-on-exec标志,默认值为0 (系统默认,在exec时不关闭) 值为1(在exec时关闭该文件)
关于close-on-exec标志
dup/dup2 和 fcntl使用F_DUPFD标志 获取的新的文件描述符 都会将close-on-exec标志清空
fcntl 使用F_DUPFD_CLEXEC参数 会复制旧的描述符的close-on-exec标志
oldfd = open("stdout_file", O_CREAT | O_RDWR, 0777);
flags = fcntl(oldfd, F_GETFD); //获取标志 默认close-on-exec为关闭状态
flags |= FD_CLOEXEC; //打开close-on-exec
fcntl(oldfd, F_SETFD, flags); // 设置新的flags
flags = fcntl(oldfd, F_GETFD); //获取标志 默认close-on-exec为关闭状态
dupfd2 = dup2(oldfd, 4); //复制文件描述符从4开始
flags = fcntl(dupfd2, F_GETFD); //获取标志 dup2返回的新的描述符清空close-on-exec标志位
dupfd = dup(oldfd);
flags = fcntl(dupfd, F_GETFD); //获取标志 dup返回的新的描述符清空close-on-exec标志位
newfd = fcntl(oldfd, F_DUPFD, 4); //新的文件描述符大于等于4
flags = fcntl(newfd, F_GETFD); //获取标志 返回的新的描述符清空close-on-exec标志位
newfd = fcntl(newfd, F_DUPFD_CLOEXEC);
flags = fcntl(newfd, F_GETFD); //获取标志 返回的新的描述符复制旧的描述符的lose-on-exec标志位
首先来查看文件属性结构体 struct stat
struct stat {
mode_t st_mode; //文件类型及权限
ino_t st_ino; //inode值
dev_t st_rdev; //设备号
nlink_t st_nlink; //硬链接设备数
uid_t st_uid; //用户ID
gid_t st_gid; //用户组ID
off_t st_size; //文件大小
struct timespace st_atime; //最后一次访问时间
struct timespace st_mtime; //最后一次修改时间
struct timespace st_ctime; //最后一次改变属性时间
long st_blksize; //数据块大小
long st_blocks; //数据块数量
};
struct timespace
{
time_t tv_sec; //秒
long tv_nsec; // 纳秒
}
(1) 普通文件(regular file)。 这是最常用的文件类型,这种文件包含了某种形式的数据。至于这种数据是文本还是二进制数据对于UNIX内核并无区别。
**-**rw-r–r-- 1 root root 77 4月 2 21:03 test
(2) 目录文件(directory file)。 这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。 对于一个目录文件具有读权限的任意一个进程都可以读该目录的内容,但只有内核可以直接写目录文件。 进程必须使用目录相关的函数才能更改目录
drwxr-xr-x 2 root root 60 4月 4 14:18 lightnvm
(3) 块设备文件(block special file)。这种类型的文件提供对设备(如磁盘)带缓冲的访问,每次访问以固定字节长度为单位进行。
brw-rw---- 1 root disk 8, 1 4月 4 14:18 sda1
(4) 字符设备文件(character special file)。这种类型的文件提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符设备,要么是块设备文件
crw-rw---- 1 root dialout 4, 85 4月 4 14:18 ttyS21
(5) FIFO 有名管道 ,这种类型的文件用于进程间通信。
prwxr-xr-x 1 root root 0 9月 28 2020 FIFO
(6) 套接字文件(socket) 这种类型的文件用于进程间的网络通信。
srwxrwxrwx 1 mysql mysql 0 04-19 11:12 /var/lib/mysql/mysql.sock
(7) 符号链接(symbolic link)。这种类型的文件指向另一个文件
lrwxrwxrwx 1 root root 15 4月 4 14:18 stdin -> /proc/self/fd/0
宏 | 文件类型 |
---|---|
S_ISREG() | 普通文件 |
S_ISDIR() | 目录文件 |
S_ISCHR() | 字符设备文件 |
S_ISBLK() | 块设备文件 |
S_ISFIFO() | 管道或FIFO |
S_ISLINK() | 符号链接文件 |
S_ISSOCK() | 套接字 |
获取当前文件的信息;需要使用绝对路径
#include
#include
#include
int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
lstat 函数,返回改符号连接的有关信息,而不是由该符号链接引用的文件的信息; 当以降序遍历目录层次结构时,需要使用lstat
struct stat *statbuf = malloc(sizeof(stat));
struct stat statbuf;
stat("/home/nfs", &statbuf);
printf("%ld\n",statbuf->st_ino);
/*************************************************************************
> File Name: stat.c
> 作者:YJK
> Mail: [email protected]
> Created Time: 2021年04月28日 星期三 09时18分21秒
************************************************************************/
#include
#include
#include
#include
int main(int argc,char *argv[])
{
int i;
struct stat buf;
char *ptr;
for (i = 1; i < argc; i ++)
{
printf("%s\n", argv[i]);
if (lstat(argv[i], &buf) < 0)
{
perror("lstat");
continue;
}
if (S_ISREG(buf.st_mode)) // 普通文件
ptr = "regular";
else if (S_ISDIR(buf.st_mode)) // 目录文件
ptr = "directory";
else if (S_ISCHR(buf.st_mode)) // 字符设备文件
ptr = "character special";
else if (S_ISBLK(buf.st_mode)) // 块设备文件
ptr = "block special";
else if (S_ISFIFO(buf.st_mode)) // 管道或 FIFO文件
ptr = "fifo";
else if (S_ISLNK(buf.st_mode)) // 链接文件
ptr= "link";
else if (S_ISSOCK(buf.st_mode)) // 套接字
ptr = "socket";
else
ptr = "unknown mode";
printf("%s\n", ptr);
}
return 0;
}
#include
#include
mode_t umask(mode_t mask);
返回值: 之前的文件模式创建屏蔽字
umask 函数为进程设置文件模式创建屏蔽字,并返回之前的值。
只改变当前进程的umask 并不改变系统的umask
对于umask命令来说, 改变的是当前终端的umask