Linux下文件I/O系统调用
一.文件类型
Linux系统下皆为文件,Linux中有七种类型文件
其中普通文件,目录文件和符号链接文件会占用磁盘空间来存储,块设备,套接字,管道是伪文件,不占用磁盘空间
命令ls -l可以查看文件类型, 例如查看 dev下log文件,开头中S表示log为套接字文件
创建管道文件命令: mkfifo
管道是Linux的七种文件类型之一,使用管道通信本质上还是以文件作为通信的媒介
有名管道与无名管道
有名管道(FIFO文件):就是 有文件名的管道, 可以用于任意两个进程间的通信
无名管道(pipe文件):就是没有文件名的管道, 只能用于父子进程之间的通信
创建有名管道,管道不能执行,不可写入, 就像吸管,一边堵死, 另外一头也传不进,就在管口堵着, 还是一个非常短,短到不能存储一丁点饮料的吸管只有读端打开的时候写端的数据才能通过管道抵达目的地, 管道本身可不会存放任何数据
文件描述符
文件描述符(file descriptor, fd)是Linux内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。程序在开始运行时,系统会自动打开三个 文件描述符,0是标准输入,1是标准输出,2是标准错误。POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号码,因此第一次打开的文件描述符一定是3.
文件描述符 |
用途 |
POSIX文件描述符 |
标准I/O文件流 |
0 |
标准输入 |
STDIN_FILENO |
stdin |
1 |
标准输出 |
STDOUT_FILENO |
stdout |
2 |
标准出错 |
STDERR_FILENO |
stderr |
其中标准输入(0)代表为键盘,在/dev/input/event0
二.文件I\O操作
我们首先运行下面这个示例程序,该程序将调用open()系统调用打开一个叫做test.txt的文件(如
果不存在则会创建该文件),然后调用write()系统调用将字符串 MSG_STR 写入到该文件中,之后调用read()系统调用读出该
文件里的内容。
1 #include <stdio.h>
2 #include <errno.h>
3 #include <string.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7
8 #define BUFSIZE 1024
9 #define MSG_STR "Hello World\n"
10
11 int main(int argc, char *argv[])
12 {
13 int fd = -1;
14 int rv = -1;
15 char buf[BUFSIZE];
16
17 fd=open("test.txt", O_RDWR|O_CREAT|O_TRUNC, 0666);
18 if(fd < 0)
19 {
20 perror("Open/Create file test.txt failure");
21 return 0;
22 }
23 printf("Open file returned file descriptor [%d]\n", fd);
24
25 if( (rv=write(fd, MSG_STR, strlen(MSG_STR))) < 0 )
26 {
27 printf("Write %d bytes into file failure: %s\n", rv, strerror(errno));
28 goto cleanup;
29 }
30
31 //memset(buf, 0, sizeof(buf));
32 if( (rv=read(fd, buf, sizeof(buf))) < 0 )
33 {
34 printf("Read data from file failure: %s\n", strerror(errno));
35 goto cleanup;
36 }
37
38 printf("Read %d bytes data from file: %s\n", rv, buf);
39
40 cleanup:
41 close(fd);
42
43 return 0;
44 }
程序运行
要运行编译生成的可执行程序使用命令 ./file_io 即可。这里的 ./ 不可省略,它用来告诉系统要运行的程序/命令
(file_io)就在当前路径下。如果把 ./ 去掉,这时就会提示“command not found”,这是因为当我们输入一条Linux命令时
(此处为 file_io),系统会查看环境变量 $PATH并在该变量所指定的路径下去找,而当前路径并不在PATH环境变量中,所以
就找不到该文件了。
下面对该程序运行的结果进行分析:
1. 程序运行时看到open()打开获取的文件描述符为3。这是因为,系统在运行时默认会打开三个文件描述符:标准输入
(0)、标准输出(1)、标准出错(2),而POSIX标准规定打开的文件描述符是当前进程中可用的文件描述符最小的一个,
所以为3。
2. 我们通过cat test.txt 命令查看文件内容验证“Hello World”字符串确实被wirte()系统调用写入到了文件中,但在接
下来调用read()尝试从文件中读取文件内容时却发现只读到了 0 个字节,明显文件中已经有了内容为什么读不到呢?
这点将在接下来的内容中分析。
3. 看到红色箭头指向的地方,我们读到的内容为0。为什么这里会打印乱码?这是因为我们在程序中定义的buf是局部变
量存放在栈中,而栈中的数据是随机数据,所以这里会显示两个乱码字符。所以在使用局部变量之前,我们都应该对
其进行初始化。
程序注解:
1~6行 是在程序中调用函数的头文件包含,具体要包含哪些头文件我们都可以通过man手册来查找;
8~9行 定义了两个宏,其中MSG_STR是我们希望写入到文件中的内容,他是一个常量,会保存到程序的只读数据段
中(Rodata);
11行 开始定义main函数,它是整个程序的入口程序。这里的main函数带两个参数,其中argc是命令行参数的个数,
argv是命令参数的字符串列表;
13行 定义了文件描述符fd, 他应该是一个非负的整数,这里我们将其初始化为-1,这是设置默认让函数的返回值出
错,如果在接下来的代码中没有对其进行修改就是用的话就会程序抛错;
14行 定义一个变量rv(return value)用来保存函数的返回值;
15行 定义了一个buffer,用来存放从文件中读到的数据,因为数据是一个一个字节的,所以我们一般都是定义成
char类型的数组;
17~22行 调用 open()系统调用并返回一个文件描述符保存在fd中,如果fd<0则说明文件打开失败了,通过perror()
函数可以打印系统调用出错的具体原因,在这里大家要注意perror()的用法,他的参数只能跟一个字符串提示符;
23行 调试打印一下fd返回的文件描述符,如果程序运行异常,我们经常会在程序中加入printf来调试错误,这是程序
出错调试最常用的方法;
25~29行 调用write()系统调用将字符串“Hello World\n”的内容写入到文件中去,函数调用如果出错则返回-1,这
时我们打印相应的错误原因;
31行 这里我们故意将改行注视掉,这样才能看到程序运行时出现即使读到0个字节,也会打印乱码字符串的bug。如
果我们把这行注视掉,那么buf中的内容在使用之前会被清零,这时就不会出现乱码的bug了;32~36行 这里我们调用read()系统调用,从文件中读取内容放入到buf中去,如果出错则打印出错的信息。当然在程
序运行结果中我们发现并没有读到数据,接下来我们会分析其中原因;
38行 我们调试打印一下从文件中读到多少个字节的数据及其内容;
40~41行 这里定义了一个goto的标号cleanup,并调用系统调用close()来关闭打开的文件描述符。这是因为在程序
中read()、write()出错后,我们都应该先关闭文件描述符,然后再退出程序。但如果每个出错处理都这样写代码就比
较冗余,我们今后在编程时会发现,很多程序都会像28行和35行这样,用goto来进行统一的出错处理;
43行 因为我们main函数原型定义返回值类型是int,所以返回一个int类型的整数。需要注意的是其他任何普通函数在
任何位置调用return()只会导致本函数返回,而main()函数在任何位置调用return()都会导致程序退出进程终止。其实
main()函数里的return()会调用exit()函数,而在任何函数的任何位置如果调用 exit()将会导致程序退出。所以在写代
码时,return和exit不能混用,如果你想让整个程序退出就调用exit(),如果你只是想函数返回则只能调用return。另
外,在main()函数调用return返回的值会传给exit(),譬如return 1则会将 1 这个值传给 exit(), 即exit(1)。而这里的 1
就是进程退出的标志,在程序运行结束后,我们可以通过 “echo $?”命令来查看程序退出的返回值。
出错处理:
对Linux而言绝大部分情况下,系统调用出错都会返回-1,而成功则返回0或其相应返回值。Linux中系统调用的错误原因都
存储于在int errno中,errno由操作系统维护,存储就近发生的错误,即下一次的错误码会覆盖掉上一次的错误,当然只有在
系统调用或者调用lib函数时出错(因为有些库函数会调用系统调用),才会置位errno。在头文件“/usr/include/asm
generic/errno-base.h”定义了常见的错误原因errno整形值。大家可以看看这个头文件的定义,了解一下常见的错误原因。
因为系统调用出错的原因是以整形值的形式保存在errno中,则个整形值errno对程序员非常不友好,我们更多的是希望看
到字符串形式的错误提示,这时经常用到的两个错误处理函数是 perror()和strerror()。下面是这两个函数的原型。
void perror(const char *s)
char *strerror(int errno);
其中perror()的使用方法非常简单,只需要一个字符串参数s即可。他会将错误的原因打印到标准输出上,其内容是字符串s
后面紧跟 冒号和字符串形式的错误原因。需要注意的是字符串s 里不要加换行符,因为错误原因里面会带换行。
因为perror()的使用功能比较简单且只能打印到标准输出上,对于要进行格式化控制的输出或想写入到日志文件中时就不能
处理了。这时我们会使用另外一个函数strerror(),它用来将整形类型的错误原因errno装换成相应的字符串形式,这样在任
何使用字符串的地方我们都可以使用它。所以在今后的编程中,我们更多的是使用strerror()而不是perror()。
示例程序:
#include <stdio.h> //printf()和perror()在该头文件中声明
#include <errno.h> //errno系统全局变量在该头文件中声明
#include <string.h> //strerror()在该头文件中声明
#include <sys/types.h> //open()系统调用需要的三个头文件
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
char *file_name = "/test.txt"; //根目录下并不存在该文件,在open()打开时会失败
int fd=-1;
fd=open(file_name, O_RDONLY, 066);
if( fd < 0 )
{
perror("Open file failure");
printf("Open file %s failure: %s\n", file_name, strerror(errno));
return 0;
}
close(fd);
}
三.文件I\O操作函数
1. open()系统调用
int open(const char *path, int oflag, ... /*mode_t mode*/);
open()系统调用用来打开一个文件,并返回一个文件描述符(file description), 并且该文件描述符是当前进程最小、未使用的
文件描述符数值。
参数: path: 要打开的文件、设备的路径
oflag: 由多个选项进行“或”运算构造oflag参数 。
必选: O_RDONLY (只读)、 O_WRONLY(只写)、 O_RDWR(读写)
可选: O_APPEND 每次写时都追加到文件的尾端。
O_CREAT 文件不存在则创建它,使用该选项需要第三个参数mode
O_TRUNC 如果文件存在,而且为只写或读写成功打开,则将其长度截取为0;
O_NONBLOCK 如果path是一个FIFO、块设备、字符特殊文件则此选项为文件的本次打开和后续的I/O操作
设置非阻塞模式方式。
O_EXEC、O_SEARCH、O_CLOEXEC、O_NOCTTY....
mode: oflag带O_CREAT选项时可以用来创建文件,这时必须带该参数用来指定创建文件的权限模式,如066。 否则不
需要。使用示例代码:
int fd;
fd = open(“text.txt”, O_RDWR|O_CREAT|O_TRUNC, 0666);
fd = open(“text.txt”, O_WRONLY|O_APPEND);
2. create()系统调用
int creat(const char *path, mode_t mode);
此函数用来创建一个新文件并返回其fd。它等价于 open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
int fd;
fd=creat(“text.txt”, 0644);
3. close()系统调用
int close(int fd);
该函数用来关闭一个打开的文件描述符,关闭一个文件时还会释放该进程加在该文件上的所有记录锁。当一个进程终止时,
内核将会自动关闭它所有打开的文件。
4. write()系统调用
ssize_t write(int fd, const void *buf, size_t nbytes);
write()函数用来往打开的文件描述符fd指向的文件中写入buf指向的数据,其中nbytes指定要写入的数据大小。如果返回值
<0则说明写入出错,譬如尝试往一个只读的文件中写入则会抛错,错误的原因系统会保存到errno变量中去。如果>0则为实
际写入的数据大小。
5. lseek()系统调用
off_t lseek(int fd, off_t offset, int whence);
我们在从文件里读出内容,或往文件写如内容的时候都有一个起始地址,这个起始地址就是当前文件偏移量,当我们对文件
进行读写的时候都会使文件偏移量往后偏移。这点就类似于我们打开记事本开始编辑文本时的光标,我们读或写入时从光标
所在位置开始读写,每读写一个字节都会使光标往后偏移。通过lseek()这个函数我们可以调整文件偏移量的地址。
其中 whence 可以是以下三个值:
whence 位置
SEEK_SET 文件头
SEEK_CUR 当前位置
SEEK_END 文件尾
而offset就是相对于whence 的偏移量,譬如:
lseek(fd, 0, SEEK_SET); 将文件偏移量设置到了文件开始的第一个字节上;
lseek(fd, 0, SEEK_END); 将文件偏移量设置到文件最后一个字节上;
lseek(fd, -1, SEEK_END); 将文件偏移量设置到文件最后的倒数第一个字节上;
6. read()系统调用
ssize_t read(int fd, void *buf, size_t nbytes);
read()函数用来从打开的文件描述符对应的文件中读取数据放到buf指向的内存空间中去,最多不要超过nbytes个字节,这里
的nbytes一般是buf剩余的空间大小。如read成功,则返回实际读到的字节数(由nbytes或读到文件尾决定,其中EOF宏用
来判断是否到了文件尾),如果返回值小于0则表示出错,如尝试读一个没有权限读的文件时就会抛错。
7. dup() 和 dup2()系统调用
int dup(int fd);
int dup2(int fd, int fd2);
这两个函数都可以用来复制一个新的文件描述符来指向fd对应的文件。这两个系统调用经常用在标准输入、标准输出、标准
出错重定向。
dup()返回的新文件描述符一定是当前可用文件描述符中的最小数值;
dup2可以用fd2参数来指定新的文件描述符。如果fd2已经打开,则先关闭。如fd等于fd2, 则dup2返回fd2, 而不关闭它。
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv)
{
int fd = -1;
fd = open("std.txt", O_RDWR|O_CREAT|O_TRUNC, 0666);
if(fd < 0)
{
printf("Open file failure: %s\n", strerror(errno));
return ;
}
dup2(fd, STDIN_FILENO); //标准输入重定向到 std.txt 文件中去
dup2(fd, STDOUT_FILENO); //标准输出重定向到 std.txt 文件中去
dup2(fd, STDERR_FILENO); //标准出错重定向到 std.txt 文件中去
printf("fd=%d\n", fd);
close(fd);
}
在程序运行时我们可以看到 printf() 打印的信息在执行时并没有打印,而是输出到文件 std.txt中去了,这是因为标准输出已
经被重定向了。
8. stat()和fstat()系统调用
int stat(const char * restrict path, struct stat *restrict buf);
int fstat(int fd, struct stat *buf);
这两个函数都是用来返回文件或目录的相关信息,只是stat()的第一个参数是文件名,而fstat()的第一个参数是文件打开的相
应文件描述符。其中struct stat结构体的定义如下:
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
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 */
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 */
};
9. access()系统调用int access(const char *path, int mode);
access()可以用来测试文件是否存在或测试其权限位,其中第一个参数path是相应的文件路径名,第二个参数是要测试的模
式。其中mode说明如下:
模式 说明
R_OK 测试读许可权
W_OK 测试写许可权
X_OK 测试执行许可权
F_OK 测试文件是否存在
#include <stdio.h>
#include <unistd.h>
#define TEST_FILE "access.c"
int main(void)
{
if( access(TEST_FILE, F_OK)!=0 )
{
printf("File %s not exist!\n", TEST_FILE);
return 0;
}
printf("File %s exist!\n", TEST_FILE);
if(access(TEST_FILE, R_OK)==0)
printf("READ OK\n");
if(access(TEST_FILE, W_OK)==0)
printf("WRITE OK\n");
if(access(TEST_FILE, X_OK)==0)
printf("EXEC OK\n");
return 0;
}
10. unlink()系统调用
该系统调用可以用来删除文件,其本质是让文件的链接记数自减。调用该函数将path指定的文件的链接数减1,如果对该文
件还有其他链接存在,则仍可以通过其他链接访问该文件的数据。只有当链接记数达到0时,该文件的内容才可被删除。如果
有进程打开了该文件,其内容也不能被删除。关闭一个文件时,内核首先检查打开该文件的进程个数,如果这个记数达到0,
内核再去检查它的链接记数,如果记数也是0,那么就删除该文件内容。
11. rename()系统调用
int rename(const char *oldname, const char *newname);
该系统调用用来将文件重命名。文件夹操作相关系统调用