文件操作是Linux系统中最常见的操作之一,关于文件的输入输出操作,分为基于文件描述符的I/O操作基于流的I/O操作。本篇介绍基于文件描述符的I/O操作。

文件类型

首先介绍下Linux系统中文件的各种类型与符号表示。

符号 文件类型 符号 文件类型
- 普通文件 l 链接文件
d 目录文件 p 管道文件
c 字符设备文件 s 套接字文件
b 块设备文件

基于文件描述符的I/O操作

文件描述符

Linux操作系统内核利用文件描述符来访问文件,文件描述符是一个非负整数,用于描述被打开文件的索引值,它指向该文件的相关信息记录表。

文件描述符的有效范围是0到1023,其中0、1、2分别用于标准输入、标准输出和标准出错。

类型 文件描述符 宏定义 说明
标准输入 0 STDIN_FILENO 命令的输入,默认为键盘,也可以是文件或其它命令的输出
标准输出 1 STDOUT_FILENO 命令的输出,默认为屏幕,也可以是文件
标准出错 2 STDERR_FILENO 命令出错信息的输出,默认为屏幕,也可以是文件

文件重定向

重定向标准输出

不使用系统标准输出的默认设备,而是将输出结果直接写在一个新的文件中,命令格式如下:

1
2
3
4
command 1> filename    # 把标准输出重定向到filename文件中
command > filename # 把标准输出重定向到filename文件中
command >> filename # 把标准输出重定向到filename文件中,方式是追加在现有内容的后面
> myfile # 创建一个长度为0的空文件

command代表用户所熟悉的shell命令

重定向标准输入

不使用系统标准输入的默认设备,而是引用其它文件的内容或是其它命令的输出,命令格式如下:

1
2
3
command < filename         # 以filename文件的内容作为command命令的标准输入
command < file1 > file2 # 以file1文件的内容作为command命令的标准输入,并以file2文件的内容作为command命令的标准输出
command << delimiter # 从标准输入中读入,直到遇到delimiter分界符

重定向标准出错

将系统指向的错误信息重定向到一个文件中,而不使用默认的显示器等输出设备,命令格式如下:

1
2
command 2> filename     # 把标准出错信息重定向到filename文件中
command 2>> filename # 把标准出错信息重定向到filename文件中(追加)

文件的创建、打开、关闭

open()函数

调用该函数可以打开或创建一个文件,函数原型为:

1
2
3
4
5
#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);

运行成功返回文件描述符,运行出错返回-1。

参数flag用于描述文件的打开方式。

flags取值 含义
O_RDONLY 以只读方式打开文件
O_WRONLY 以只写方式打开文件
O_RDWR 以读写方式打开文件
O_CREATE 若打开的文件不存在则创建此文件(使用此选项需同时使用mode参数说明存取许可权位)
O_EXCL 若同时指定了O_CREAT,且文件已经存在,则导致调用出错
O_TRUNC 若文件存在,且为只读或只写方式打开,则将其长度截断为0
O_NOCTTY 若文件指向的是终端设备(tty),则不将此设备分配作为此进程的控制终端
O_APPEND 每次写时都加到文件的尾端
O_NONBLOCK 若文件指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为此文件的本次打开操作和后续的I/O操作设置为非阻塞方式
O_NONELAY 功能不完善的O_NONBLOCK,通常不使用
O_SYNC 只在数据被写入外存或其它设备之后操作才返回

参数mode用于指定所创建文件的权限。

mode取值 对应八进制数 含义
S_ISUID 04000 设置用户识别号
S_ISGID 02000 设置识别号
S_SVTX 01000 粘贴位
S_IRUSR 00400 文件所有者权限位
S_IWUSR 00200 文件所有者的权限位
S_IXUSR 00100 文件所有者的执行权限位
S_IRGPR 00040 同用户组权限位
S_IWGPR 00020 同用户组的权限位
S_IXGRP 00010 同用户组的执行权限位
S_IROTH 00004 其它用户权限位
S_IWOTH 00002 其它用户的权限位
S_IXOTH 00001 其它用户执行权限位

使用open函数打开或创建一个文件,open_file.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define FLAGS O_WRONLY|O_CREAT|O_TRUNC
#define MODE S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH

int main(void)
{
const char *pathname;
int fd;
char pn[30];
printf("Input the pathname[<30 strings]:");
gets(pn);
pathname = pn;
if((fd = open(pathname, FLAGS, MODE)) == -1)
{
printf("error, open file failed!\n");
exit(1);
}
printf("OK, open file successful!\n");
printf("fd=%d\n", fd);

return 0;
}

编译后执行:

1
2
3
4
$ ./open_file
Input the pathname[<30 strings]:./hello.txt
OK, open file successful!
fd=3

程序在当前目录创建了hello.txt文件,进一步查看该文件的信息:

1
2
ls -l hello.txt
-rwxr-xr-x 1 deeplearning deeplearning 0 12月 20 19:59 hello.txt

可以看到文件的权限为rwxr-xr-x,与程序中的MODE宏定义一致。

create()函数

用于创建文件,函数原型为:

1
2
3
4
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);

运行成功返回以只写方式打开的文件描述符,运行出错返回-1。

close()函数

用于关闭文件,函数原型为:

1
2
3
#include <unistd.h>
int close(int fd);

运行成功返回0,运行出错返回-1。

文件的定位

每个已打开的文件都有一个与其相关联的“当前文件位移量”,它是一个非负整数,用以度量从文件开始处计算的字节数。可以调用lseek()函数显示地定位一个打开文件,函数原型为:

1
2
3
4
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

运行成功返回文件位移量,运行出错返回-1。

whence取值 含义
SEEK_SET 将该文件的位移量设置为距文件开始处offset个字节
SEEK_CUR 将该文件的位移量设置为其当前值加offset个字节,offset可正可负
SEEK_END 将该文件的位移量设置为文件长度加offset个字节,offset可正可负

测试标准输入能否被设置位移量,offset_test.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
if(lseek(0, 0, SEEK_CUR) == -1)
printf("can't seek!\n");
else
printf("seek OK!\n");
exit(0);
}

编译后执行:

1
2
3
4
5
$ ./offset_test
can't seek!
$ ./offset_test < ./hello.txt
seek OK!

可以看出,对于标准输入,一般不能设置位移量,而用户创建的一般文件可以设置位移量。

文件的读写

read()函数

用read()函数从打开文件中读取数据,函数原型为:

1
2
3
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

运行成功返回读到的字节数,若已到文件尾返回0,运行出错返回-1。

参数fd表示文件描述符,参数buf为指向缓冲区的指针,参数count表示本次操作将要读取的字节数。

实际读取的字节数有时会少于要求读取的字节数:

  • 读普通文件时,在读到要求的字节数之前到达文件尾
  • 从终端设备读时,通常一次读取一行
  • 从网络中读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数

write()函数

用write()函数向打开文件中写数据,函数原型为:

1
2
3
#include <unistd.h>
ssize_t write(int fd, void *buf, size_t count);

运行成功返回已写的字节数,运行出错返回-1。

参数fd表示文件描述符,参数buf为指向缓冲区的指针,参数count表示本次操作将要写入的字节数。

函数返回值通常与参数count的值相同,否则表示出错。出错的原因通常是磁盘已写满或超过了对一个给定进程的文件长度限制。

使用write函数向文件写入数据,write_file.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define FILENAME "./hello.txt"
#define SIZE 80
#define FLAGS O_RDWR|O_APPEND

int main(void)
{
int count;
int fd;
char write_buf[SIZE];
const char *pathname = FILENAME;
if((fd=open(pathname, FLAGS)) == -1)
{
printf("error, open file failed!\n");
exit(1);
}
printf("OK, open file successful!\n");
printf("Begin Write:\n");
gets(write_buf);
count = strlen(write_buf);
if(write(fd, write_buf, count) == -1)
{
printf("error, write file failed!\n");
exit(1);
}
printf("OK, write %d strings to file!\n", count);

return 0;
}

编译后执行:

1
2
3
4
5
6
$ ./write_file
OK, open file successful!
Begin Write:
hello,linux C!
OK, write 14 strings to file!

查看hello.txt:

1
2
3
$ cat hello.txt
hello,linux C!

文件的属性操作

改变文件访问权限

chmod()、fchmod()这两个函数使用户可以更改现存文件的存取许可权:

1
2
3
4
5
#include <sys/types.h>
#include <sys/stat.h>
int chmod(conts char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);

运行成功返回0,运行出错返回-1。

  • chmod(),在指定的文件上进行操作,pathname指定了文件的绝对或相对路径名
  • fchmod(),在已打开的文件上进行操作,fd是文件描述符,mode为文件的权限

使用chmod函数改变文件的访问权限,change_mode.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define FILENAME "./hello.txt"
#define MODE 0700

int main(void)
{
const char *pathname = FILENAME;
if(chmod(pathname, MODE) == -1)
{
printf("error, change failed!\n");
exit(1);
}
printf("OK, change successful!\n");

return 0;
}

编译后执行:

1
2
3
$ ./change_mode
OK, change successful!

进一步查看该文件的信息:

1
2
3
$ ls -l hello.txt
-rwx------ 1 deeplearning deeplearning 14 12月 20 20:09 hello.txt

可以看到文件的权限为rwxr-----,与程序中的MODE宏定义(0755)一致。

改变文件所有者

有3个函数可以改变一个文件的所有者识别号和用户组识别号,函数原型如下:

1
2
3
4
5
6
#include <sys/types.h>
#include <unistd.h>
int chown(const char *pathname, uid_t owner, git_t group);
int fchown(int fd, uid_t owner, git_t group);
int lchown(const char *pathname, uid_t owner, git_t group);

运行成功返回0,运行出错返回-1。

  • chown(),修改指定文件的所有者
  • fchown(),修改已打开文件的所有者
  • lchown(),修改符号链接文件本身的所有者

重命名

对文件或目录文件重命名,函数原型:

1
2
3
#include <stdio.h>
int rename(const char *oldname, const char *newname);

运行成功返回0,运行出错返回-1。

修改文件的长度

截短文件可以调用truncate()和ftruncate(),函数原型如下:

1
2
3
4
5
#include <sys/types.h>
#include <unitd.h>
int truncate(const char *pathname, off_t len);
int ftruncate(int fd, off_t len);

运行成功返回0,运行出错返回-1。

文件的其它操作

stat、fsat、lstat函数

Linux系统中所有文件都有一个与之对应的索引节点,该节点包含了文件的相关信息,这些信息被保存在stat结构体中,可以调用下面3个stat函数来返回文件的信息:

1
2
3
4
5
6
#include <sys/types.h>
#include <unitd.h>
int state(const char *pathname, struct stat *sbuf);
int fstate(int fd, struct stat *sbuf);
int lstate(const char *pathname, struct stat *sbuf);

运行成功返回0,运行出错返回-1。

stat结构体基本形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct stat
{
mode_t st_mode; // 文件类型&模式
ino_t st_ino; // i-node号
dev_t st_dev; // 设备号
dev_t st_rdev; // 设备号(特殊文件)
nlink_t st_nlinkl;// 链接号
uid_t st_uid; // 用户ID
gid_t st_gid; // 组ID
off_t st_size;
time_t st_atime; // access
time_t st_mtime; // modification
time_t st_ctime; // file station change
unsigned long st_blksize;
unsigned long st_blocks;
}

dup、dup2函数

这两个函数可以用来复制一个现存的文件描述符,原型如下:

1
2
3
4
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);

运行成功返回新的文件描述符,运行出错返回-1。

fcntl函数

该函数可以改变已打开文件的性质,原型如下:

1
2
3
4
5
6
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);

运行成功依赖于cmd,运行出错返回-1。

cmd取值 相应操作
F_DUPFD 复制一个现存的文件描述符
F_GETFD 获得文件描述符标记
F_SETFD 设置文件描述符标记
F_GETFL 获得文件状态标记
F_SETFL 设置文件状态标记
F_GETOWN 获得异步I/O有权
F_SETOWN 设置异步I/O有权
F_GETLK 获得记录锁
F_SETLK 设置记录锁,不等待
F_SETLKW 设置记录锁,必须时等待

sync、fsync函数

为保证磁盘上实际文件系统与缓存中内容的一致性,Linux系统提供了两个系统调用函数,原型如下:

1
2
3
4
#include <unistd.h>
void sync(void);
int fsync(int fd);

运行成功返回0,运行出错返回-1。

特殊文件的操作

目录文件的操作

mkdir、rmdir函数

mkdir用于创建目录,原型如下:

1
2
3
4
#include <sys/types.h>
#include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode);

运行成功返回0,运行出错返回-1。

rmdir用于删除目录,原型如下:

1
2
3
#include <unistd.h>
int rmdir(const char *pathname);

运行成功返回0,运行出错返回-1。

opendir 、closedir、readdir函数

  • opendir用于打开目录,原型如下:
1
2
3
4
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *pathname);

运行成功返回指针,运行出错返回NULL。

  • closedir用于关闭目录,原型如下:
1
2
3
4
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dp);

运行成功返回0,运行出错返回-1。

  • readdir用于读取目录,原型如下:
1
2
3
4
#include <sys/types.h>
#include <dirent.h>
struct dirent *readdir(DIR *dp);

运行成功返回指针,运行出错返回NULL。

函数返回值指向的结构体指针定义为:

1
2
3
4
5
6
struct dirent
{
ino_t d_ino;
char d_name[NAME_MAX+1];
}

chdir、fchdir、getcwd函数

进程调用chdir或fchdir函数可以更改当前工作目录,函数原型如下:

1
2
3
4
#include <unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);

运行成功返回0,运行出错返回-1。

这两个函数,可以分别用pathname或文件描述符来指定新的当前工作目录。

获取当前工作目录的绝对路径,使用getcwd函数,函数原型为:

1
2
3
#include <unistd.h>
char *getcwd(char *buf, size_t size);

运行成功返回buf,运行出错返回NULL。

向此函数传递两个参数,一个是缓存地址buf,另一个是缓存的长度size,该缓存必须有足够的长度以容纳绝对路径名再加上一个NULL终止符,否则返回出错。

改变并获取当前的工作目录,change_path.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

#define SIZE 30

int main(void)
{
char newpath[SIZE];
char buf[SIZE];
printf("Input the new pathname[<30 strings]:");
gets(newpath);
if(chdir(newpath) == -1)
{
printf("error, change directory failed!\n");
exit(1);
}
printf("OK, change directory successful!\n");
if(getcwd(buf, SIZE) == NULL)
{
printf("error, getcwd failed!\n");
exit(1);
}
printf("cwd=%s\n", buf);

return 0;
}

先查看当前的工作目录:

1
2
3
$ pwd
/home/deeplearning/dcj/linuxCTest/fileIO

编译后执行:

1
2
3
4
5
$ ./change_path
Input the new pathname[<30 strings]:/home/deeplearning
OK, change directory successful!
cwd=/home/deeplearning

再次使用pwd查看当前的工作目录,理论上应该已经切换到新的目录(但是我这里测试没有切换成功,原因未知)。

链接文件的操作

硬链接

  • 创建一个硬链接使用link函数,原型如下:
1
2
3
#include <unistd.h>
int link(const char *pathname1, const char *pathname2);

运行成功返回0,运行出错返回-1。

此函数创建一个新目录项pathname2,它引用现存文件pathname1,若pathname2已存在则返回出错。

硬链接要求两文件路径位于同一文件系统中,且只有超级用户root才可以创建指向一个目录的新链接。

  • 删除一个硬链接使用unlink函数,原型如下:
1
2
3
#include <unistd.h>
int unlink(const char *pathname);

运行成功返回0,运行出错返回-1。

  • 也可以使用remove函数解除对一个文件或目录的连接。对于文件,remove的功能与unlink相同,对于目录,remove的功能与rmdir相同。
1
2
3
#include <stdio.h>
int remove(const char *pathname);

运行成功返回0,运行出错返回-1。

符号链接

符号链接是对一个文件的间接指针。

symlink函数用于创建一个符号链接,原型如下:

1
2
3
#include <unistd.h>
int symlink(const char *actualpath, const char *sympath);

运行成功返回0,运行出错返回-1。

参考:《精通Linux C编程》- 程国钢