@TOC
前言
在学习LinuxC语言的时候首先需要学习的就是文件的操作,因为在Linux中一切皆为文件,所以所以C语言对文件操作是Linux开发的基础内容,这篇文章主要是讲解一下C语言对Linux文件操作的一些基本函数和操作。
一、文件描述符
在学习C语言对文件的操作的时候首先我们来讲解一下什么是文件描述符。
当我们在Linux中打开文件的时候,系统(内核)会返回一个文件描述符,文件描述符用来指定已经打开的文件。这个文件描述符相当于这个文件已经打开文件的符号,文件描述符是非负整数,是文件的标识,操作这个文件描述符相当于操作这个描述符所指定的文件,所以可以理解为文件描述符就是那一个文件的另一种显示方式,而这个文件描述符是一些整数。
而在程序运行起来后(每个进程)都有一张文件描述符的表,标准输入、标准输出、标准错误输出设备文件被打开,对应的文件描述符0、1、2记录在表中。程序运行起来后这三个文件描述符是默认打开的。
这三个文件描述符都有对应的宏定义和值:
|宏定义| 文件描述符 | 描述 |
|--|--|--|
|STDIN_FILENO | 0 | 标准输入的文件描述符 |
| STDOUT_FILENO | 1 | 标准输出的文件描述符 |
| STDERR_FILENO | 2 | 标准错误的文件描述 |
文件描述符有个数限制的,我们可以通过下面的命令查看一下当前系统能够打开的最大文件描述符的个数:
cat /proc/sys/fs/file-max
当然我们也是可以修改默认设置最大打开文件个数:
ulimit -n 4096
查看当前默认设置最大打开文件个数1024
ulimit -a
二、常用的文件IO函数
学习完文件标识符的基本概念后就可以学习文件操作的IO函数了。
1.open函数
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
mode:这个参数,只有在文件不存在时有效,指新建文件时指定文件的权限
返回值:
成功:返回打开的文件描述符
失败:-1
这个函数有两个参数的和三个参数的,两个参数的一般是对文件进行读取或者是写入存在的函数中,三个参数主要是进行一些其它操作的。
标识flags中填写的内容如下:
| 取值 | 含义 |
| -------- | ---------------------- |
| O_RDONLY | 以只读的方式打开 |
| O_WRONLY | 以只写的方式打开 |
| O_RDWR | 以可读、可写的方式打开 |
还可以写一些可选项:
| 取值 | 含义 |
| ---------- | ------------------------------------------------------------ |
| O_CREAT | 文件不存在则创建文件,使用此选项时需要使用mode说明文件的权限 |
| O_EXCL | 如果同时指定了O_CREAT,则文件已经存在,则会出错 |
| O_TRUNC | 如果文件存在,则清空文件内容 |
| O_APPEND | 写文件时,数据添加到文件末尾 |
| O_NONBLOCK | 对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O |
如果需要将这些选项联合使用就需要在之间加上|
来进行连接.
如果这个文件不存在那就需要创建一下,而在Linux中创建文件是需要给这个文件一个权限(mode)的,而权限的宏选项如下:
| 取值 | 八进制 | 含义 |
| ------- | ------ | -------------------------------------- |
| S_IRWXU | 00700 | 文件拿使用者的读、写、可执行权限 |
| S_IRUSR | 00400 | 文件所有者的读权限 |
| S_IWUSR | 00200 | 文件所有者的写权限 |
| S_IXUSR | 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 | 其它用户的可执行权限 |
这里需要注意一个问题,就是最终权限是用mode & ~umask
才能得到的,你可以在你的shell中执行一下:
umask
看看你的umask
的值为多少,然后用这个取反后与上你需要给的权限才能得到最终的权限。
比如说我现在要打开个文件,然后是写,如果文件不存在,那么就创建一下,创建的权限为664
,如果文件存在,那就清空里面的内容,那么open
函数的使用如下:
open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
但你可能会觉得上面的方法很臃肿,那有什么办法可以让这个更简单呢?
其实可以把mode
值换成八进制就可以解决这个问题:
open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0664);
这样也是可以的,这里就得看你的习惯了,有些人习惯写得臃肿有些喜欢写得简单。
当这个函数调用完之后会得到你打开文件后的文件描述符,操作这个文件描述符相当于操作这个文件。
2.close函数
打开文件后对应的就是关闭文件,关闭文件使用的函数是close
函数,函数的原型如下:
#include <unistd.h>
int close(int fd);
功能:
关闭已打开的文件
参数:
fd:文件描述符,open()的返回值
返回值:
成功:0
失败:-1,并设置errno
需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所有即使用用户程序不调用close,在终止时内核也会自动关闭它打开的文件。
但对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,负责随着打开的文件越多,会占用大量文件描述符和系统资源。
而使用的方法如下:
close(fd); //fd是上面open后打开的文件描述符
3.read函数
read函数主要是用来读取文件中的内容的,但是并不是read可以随便读取你打开的文件中的内容,你需要让打开的方式为O_RDONLY
或者是O_RDWR
才可以读取文件中的内容,read
函数的原型如下:
#include <unistd.h>
size_t read(int fd, void* buf, size_t count);
功能:
把指定数目的数据到内存(缓冲区)
参数:
fd:文件描述符
buf:内存首地址
count:读取的字节个数
返回值:
成功:实际读取的字节个数
失败:-1
调用完这个函数后读取的字节数和实际读取的字节数是不一样的,比如说你的文件中的字节数为100,而你这里指定读入200个字节,函数的返回值只能为100,而不能为200,毕竟这个文件中没有200个字节的文件。
如果返回为0,那证明读取完毕,就是这个文件没有数据了,所以可以通过读取的返回值是否为0来判断这个文件是否被读取完毕。
下面是读取文件中内容的例子:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int fd = -1; // 文件描述符
char buf[10]; // 储存数据的变量
int size; // 读取字节大小
fd = open("/tmp/filename", O_RDONLY);
if (fd == -1){
perror("open"); // 错误处理函数,后面会说的
return -1;
}
size = read(fd, buf, 10);
if (size == -1){
perror("read");
return -2;
}
printf("%s\n", buf);
close(fd);
return 0;
}
上面的代码是一个非常简单的读取文件中内容的代码,但是需要注意就是读取的文件必须得存在,否则就会报错。
这里还有一个问题,第一个参数是文件描述符,那我直接在第一个选项中写上0
可以吗?
答案是可以的,因为前面说过,文件描述符是整形,而0、1、2这三个是标准的文件描述符,所以直接在这写上这些标准的文件描述符是完全可以的。
3.1 练习1
这里有一个练习,不使用scanf()
输入函数,让用户输入的内容原封不动的输出到屏幕中,输入%
结束,代码就如下:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(){
char buf;
int size;
while(1){
size = read(0, &buf, 1);
if (size == -1){
perror("read");
return -1;
}
if (buf == '%'){
break;
}
printf("%c", buf);
}
return 0;
}
4.write函数
write函数是写入函数,就是往文件中写入内容的函数,这个函数的原型如下:
#include <unistd.h>
size_t write(int fd, const void* buf, size_t count);
功能:
把指定数目的数据写到文件(fd)
参数:
fd:文件描述符
buf:数据首地址
count:写入数据的长度(字节)
返回值:
成功:实际写入数据的字节个数
失败:-1
这个函数的返回值和写入数据的长度是相同的,这里是需要注意一下的。
简单使用write函数如下:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(){
int fd = -1;
char buf[] = "hello,world";
fd = open("/tmp/filename", O_WRONLY);
if (fd == -1){
perror("open");
return -1;
}
write(fd, buf, sizeof(buf));
close(fd);
return 0;
}
上面的代码可以将buf
中的内容写入到文件中。
这里又有一个练习,和上面read
差不多。
4.1 练习2
不使用scanf()
输入函数和printf()
输出函数,让用户输入的内容原封不动的输出到屏幕中,输入%
结束,代码就如下:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(){
char buf;
int size;
while(1){
size = read(0, &buf, 1);
if (size == -1){
perror("read");
return -1;
}
if (buf == '%'){
break;
}
write(1, buf, 1);
}
return 0;
}
5.模仿cp命令
当讲完了write和read函数后就可以来模仿一下cp
命令了。首先介绍一下cp
命令,cp命令其实是复制粘贴命令,可以将文件的内容复制粘贴到另一个文件中,也可以将文件复制粘贴到文件夹中,这里只考虑文件到文件,而cp命令的用法如下:
cp [文件] [目标文件]
现在我们来使用cp命令模仿一下该命令的功能,首先需要理解这个cp功能是如何进行复制粘贴的,其实就是将文件中的内容读取出来,然后再打开目标文件后,使用写就可以将之前读取的内容写入进去了,这个就是cp命令的模仿思路。
有了思路就可以将代码写一下了:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define SIZE 512
int main(int argc, char* argv[]){
int fd_r, fd_w, size;
char buf[SIZE];
if (argv < 3){
printf("参数太少\n");
return -1;
}
else if (argv > 3){
printf("参数太多\n");
return -1;
}
fd_r = open(argv[1], O_RDONLY);
if (fd_r == -1){
perror("open fd_r");
return -2;
}
fd_w = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (fd_w == -1){
perror("open fd_w");
close(fd_r);
return -2;
}
while(1){
// 读取文件中的内容
size = read(fd_r, buf, SIZE);
// 读取失败
if (size == -1){
perror("read");
return -3;
}
//读取完毕
if (size == 0){
break;
}
//写入目标文件中
write(fd_w, buf, size);
}
// 拷贝完毕关闭文件
close(fd_w);
close(fd_r);
return 0;
}
然后编译并运行后传入参数就可以了
gcc -o main main.c
./main filename filename.txt
7.lseek函数
这个是基础中最后一个函数,也是比较复杂的一个函数,其实这个函数就是对光标的移动,但是这个函数只能对普通文件中的光标进行操作。函数原型如下:
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
功能:
改变文件的偏移量
参数:
fd:文件描述符
offset:根据whence来移动的位移量(偏移量),
可以是正数,也可以负数,如果正数,则相对于whence往右移动,如果是负数,则相对于whence
往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果移动
的字节数超过了文件末尾,再次写入时将增大文件尺寸。
whence:其取值如下:
SEEK_SET: 从文件开头移动offset个字节数
SEEK_CUR: 从当前位置移动offset个字节数
SEEK_END: 从文件末尾移动offset个字节数
返回值:
若lseek成功执行,则返回新的偏移量
如果失败,返回-1
简单理解就是offset
是光标移动的位置,whence
是光标从哪移动的位置即可。
7.1 练习3
学习完这个后可以做下面的练习了:
现在有一个文件,需要写一个程序让这个文件中的内容颠倒。比如说文件中是:abcdefg,颠倒后就是gfedcba。
这个看上去很难,但是其实逻辑很简单的,首先读取文件,然后将读取的内容颠倒,再写入文件即可,代码如下:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define SIZE 1024
int main(int argc, char* argv[]){
int fd = -1;
char buf[SIZE], a;
int size, i;
if (argc != 2){
printf("参数有问题\n");
return -1;
}
fd = open(argv[1], O_RDWR);
if (fd == -1){
printf("文件不存在");
return -2;
}
size = read(fd, buf, SIZE);
if (size == -1){
perror("read");
return -2;
}
for (i = 0; i < size / 2 - 1; i++){
a = buf[i];
buf[i] = buf[size - 2 - i];
buf[size - 2 - i] = a;
}
lseek(fd, SEEK_SET, 0);
write(fd, buf, size);
close(fd);
return 0;
}
总结
对于文件的操作其实还有其它更高级的用法,但是这里只是最基本的操作,大家多练习练习即可学会这些内容。