文件操作
打开文件
1.使用open()函数打开和创建文件
- 手册文件 man 2 open
函数头文件及函数原型
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
函数参数:
pathname:待打开文件的绝对路径和文件名。
flags:打开的旗标类型,或称模式,
O_RDONLY 只读模式打开文件
O_WRONLY 只写模式打开文件
O_RDWR 读写模式打开文件
O_CREAT 若欲打开的文件不存在则自动建立该文件
O_TRUNC 若文件存在并且以可写的方式打开时, 此旗标会令文件长度清为0,
而原来存于该文件的资料也会消失。
O_EXCL 如果O_CREAT 也被设置, 此指令会去检查文件是否存在。
文件若不存在则建立该文件,否则将导致打开文件错误.
此外, 若O_CREAT 与O_EXCL 同时设置,并且欲打开的文件为符号连接,
则会打开文件失败。
参数mode仅在flags中含有O_CREAT时有效,设定新建文文件的打开权限,有下列数种组合,
S_IRWXU 00700 权限,代表该文件所有者具有可读、可写及可执行的权限。
S_IRUSR 或S_IREAD, 00400 权限,代表该文件所有者具有可读取的权限。
S_IWUSR 或S_IWRITE, 00200 权限,代表该文件所有者具有可写入的权限。
S_IXUSR 或S_IEXEC, 00100 权限,代表该文件所有者具有可执行的权限。
S_IRWXG 00070 权限,代表该文件用户组具有可读、可写及可执行的权限。
S_IRGRP 00040 权限,代表该文件用户组具有可读的权限。
S_IWGRP 00020 权限,代表该文件用户组具有可写入的权限。
S_IXGRP 00010 权限,代表该文件用户组具有可执行的权限。
S_IRWXO 00007 权限,代表其他用户具有可读、可写及可执行的权限。
S_IROTH 00004 权限,代表其他用户具有可读的权限。
S_IWOTH 00002 权限,代表其他用户具有可写入的权限。
S_IXOTH 00001 权限,xit代表其他用户具有可执行的权限。
函数返回值: 打开文件成功,返回一个文件描述符 >2;打开失败,返回-1。
提示:使用 access()作用户认证方面的判断要特别小心, 例如在access()后再作open()空文件可能会造成系统安全上的问题。
2.使用create()函数创建并打开文件
函数原型
int creat(const char *pathname, mode_t mode);
相当于使用调用方式,
open(const char *pathname, (O_CREAT|O_WRONLY|O_TRUNC));
函数参数:
pathname 待打开文件的绝对路径和文件名。
mode 新创建文件的权限,见上面open()
函数返回值:若成功会返回新的文件描述符,若有错误发生则会返回-1。
提示:creat()无法建立特别的装置文件,如果需要请使用mknod()。
读写文件
1.使用read()函数从文件中读取数据
- 手册文件 man 2 read
函数头文件及函数原型
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
函数参数:
fd 文件指针,提供数据的文件的文件描述符,读取的数据的来源。
buf 读到的数据所存放的内存空间的起始地址,同时文件的当前读写位置向后移。
count 想要读取的数据的字节数,也是提供的存储空间字节数。
函数说明及返回值: read()会把参数fd 所指的文件传送count 个字节到buf 指针所指的内存中(暨在[0,count]区间变化)。
1.若参数count 为0,则read()不会有作用并返回0。
2.成功时,返回值为实际读取到的字节数。
3.如果返回0,表示已到达文件尾,暨碰到了EOF或是无可读取的数据。
4.此外文件读写位置会随读取到的字节移动。
5.有错误发生时则返回-1,而文件读写位置则无法预测。
提示:
read()函数负责从文件句柄中读取指定数量的字节,并将这些字节放在标量型变量中。read()函数和标准I/O函数fread()相同的方式处理I/O缓冲的。为了提高效率,read()函数并不是一次读取一个字节,而是读取一块数据并保存到临时存储区中。然后,C的fread函数与Perl的read函数会从临时缓冲区将数据一次一个字节地传送给程序。print()函数(而不是write()函数负责输出read()函数返回的实际字节。print()函数类似于C中的fwrite()函数。
附加:如果顺利 read()会返回实际读到的字节数,最好能将返回值与参数count 作比较,若返回的字节数比要求读取的字节数少,则
1. 读取普通文件时,读到文件末尾还不够 nbytes 字节。例如:如果文件只有 30 字节,
而我们想读取 100字节,那么实际读到的只有 30 字节,read 函数返回 30 。
此时再使用 read 函数作用于这个文件会导致 read 返回 0 。
2. 从终端设备(terminal device)读取时,一般情况下每次只能读取一行。
3. 从网络读取时,网络缓存可能导致读取的字节数小于 nbytes 字节。
4. 读取 pipe 或者 FIFO 时,pipe 或 FIFO 里的字节数可能小于 nbytes 。
5. 从面向记录的设备读取时,某些面向记录的设备(如磁带)每次最多只能返回一个记录。
6. 在读取了部分数据时被信号中断。读操作始于 cfo 。在成功返回之前,cfo 增加,
增量为实际读取到的字节数。
2.使用write()函数向指定文件中写入数据
- 手册文件 man 2 write
函数头文件及函数原型
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
函数参数:
fd 待写入数据的文件的描述符
buf 写入数据的起始地址
count 待写入的数据的字节数
函数说明及返回值: write()会把参数buf 所指的内存写入count 个字节到参数fd 所指的文件内。当然,文件读写位置也会随之移动。
如果顺利会返回实际写入数据的字节数,表示写了部分或者全部的数据。
当有错误发生时,返回-1,我们要根据错误的类型来处理。如果错误为EINTR表示在写时出现了中断错误。如果为EPIPE表示网络连接出现了问题。
提示:对于普通文件,写操作始于 cfo 。如果打开文件时使用了 O_APPEND,则每次写操作都将数据写入文件末尾。成功写入后,cfo 增加,增量为实际写入的字节数。
定位文件
预概念: 所有打开的文件都有一个当前文件偏移量(current file offset),以下简称为 cfo。cfo 通常是一个非负整数,用于表明文件开始处到文件当前位置的字节数。读写操作通常开始于 cfo,并且使 cfo 增大,增量为读写的字节数。文件被打开时,cfo 会被初始化为 0,除非使用了 O_APPEND 。
使用lseek()函数定位指定已打开文件的读写指针
- 手册文件 man lseek
函数头文件及函数原型
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
函数参数:
fd 待重新定位读写指针位置的文件的描述符
offset 读写指针的偏移量(可正可负可为0)
whence 读写指针的偏移位置
SEEK_SET 相对文件首部偏移,文件偏移量将被设置为 offset。
SEEK_CUR 相对文件当前读写位置偏移,文件偏移量将被设置为 cfo 加上 offset,
offset 可以为正也可以为负。
SEEK_END 相对文件尾部偏移,文件偏移量将被设置为文件长度加上 offset,
offset 可以为正也可以为负。
函数说明及返回值: 每一个已打开的文件都有一个读写位置,当打开文件时通常其读写位置是指向文件开头,若是以附加的方式打开文件(如O_APPEND),则读写位置会指向文件尾。当read()或write()时,读写位置会随之增加,lseek()便是用来控制该文件的读写位置。参数fildes 为已打开的文件描述词,参数offset 为根据参数whence来移动读写位置的位移数。当调用成功时则返回目前的读写位置,也就是距离文件多少个字节数。若有错误则返回-1。
例:
将读写位置移到文件开头时: lseek(int fildes, 0, SEEK_SET);
将读写位置移到文件尾时: lseek(int fildes, 0, SEEK_END);
想要取得目前文件位置时: lseek(int fildes, 0, SEEK_CUR);
提示:
1.Linux 系统不允许lseek()对tty 装置作用,此项动作会令lseek()返回ESPIPE。
2.如果参数 fd(文件描述符)指定的是 pipe(管道)、FIFO 或者 socket,lseek 返回 -1 并且置 errno 为 ESPIPE。 对于普通文件(regular file),cfo 是一个非负整数。但对于特殊设备,cfo 有可能是负数。因此,我们不能简单地测试 lseek 的返回值是否小于 0 来判断 lseek 成功与否,而应该测试 lseek 的返回值是否等于 -1 来判断 lseek 成功与否。
3.lseek 仅将 cfo 保存于内核中,不会导致任何 I/O 操作。这个 cfo 将被用于之后的读写操作。
4.如果 offset 比文件的当前长度更大,下一个写操作就会把文件“撑大(extend)”。这就是所谓的在文件里创造"空洞(hole)”。没有被实际写入文件的所有字节由重复的 0 表示。空洞是否占用硬盘空间是由文件系统(file system)决定的。
关闭文件
使用close函数关闭指定文件
- 手册文件 man close
函数头文件及函数原型
#include <unistd.h>
int close(int fd);
函数参数:
fd 为open()或creat()打开的文件描述符。
函数说明及返回值: 当使用完已打开的文件后若已不再需要则可使用 close()关闭该文件, 而close()会让数据写回磁盘, 并释放该文件所占用的资源. 参数fd 为先前由open()或creat()所返回的文件描述词.**返回值:若文件顺利关闭则返回0, 发生错误时返回-1.
提示:虽然在进程结束时,系统会自动关闭已打开的文件,但仍建议自行关闭文件,并确实检查返回值。
综合案例
// ./my-cp <src_file> <dst_file>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFFER_SIZE 100
int main(int argc, char *argv[])
{
if(argc != 3)
{
printf("usage : %s <src_file> <dst_file>\n",
argv[0]);
return 1;
}
int src_fd = 0;
int dst_fd = 0;
int n = 0;
char buf[BUFFER_SIZE] = {'\0'};
char *src_file = argv[1];
char *dst_file = argv[2];
// 1.open
// 1.1 以只读方式打开源文件
if((src_fd = open(src_file, O_RDONLY)) == -1)
{
perror("open src error");
return 1;
}
// 1.2 以只写方式打开目的文件
if((dst_fd = open(dst_file,
O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR)) == -1)
{
perror("open dst error");
return 1;
}
// 2. 循环从源文件中读取数据写入到目的文件中
// 直到读到源文件的尾部为止
// 2.1 read data from src_file
// 2.2 write data to dst_file
while((n = read(src_fd, buf, BUFFER_SIZE)) > 0)
{
write(dst_fd, buf, n);
}
// 3.close
close(src_fd);
close(dst_fd);
return 0;
}
// 练习:
// 实现一个相对完整版的cp程序,要求能够判断出目标文件是否存在。
// 如果存在,给出提示是否覆盖。
// 思路:
// 1.打开源文件
// 2.判断目的文件是否存在
// 3.如果目的文件存在,提示是否覆盖
// 4.如果选择覆盖,则以只写的方式打开文件,并截短文件内容(O_TRUNC)
// 5.如果选择不覆盖,则提醒输入新的保存文件名,并已只写方式打开
// 6.循环读取源文件内容,写入到目的文件中
// 7.关闭已打开的文件
// 思考题1:能否关闭标准输入文件、标准输出文件、标准出错文件?
参考资料
刘老师上课资料及网上前辈资料
计算机操作系统教程:介绍现代操作系统原理及应用