一、先来了解下什么是文件 I/O 和标准 I/O:
文件I/O:文件 I/O 称之为不带缓存的 IO(unbuffered I/O)。不带缓存指的是每个 read,write 都调用内核中的一个系统调用。也就是一般所说的低级 I/O——操作系统提供的基本 IO 服务,与 os 绑定,特定于 Linux 或 unix 平台。
标准 I/O:标准 I/O 是 ANSI C 建立的一个标准 I/O 模型,是一个标准函数包和 stdio.h 头文件中的定义,具有一定的可移植性。标准 I/O 库处理很多细节。例如缓存分配,以优化长度执行 I/O 等。标准的 I/O 提供了三种类型的缓存。
(1)全缓存:当填满标准 I/O 缓存后才进行实际的 I/O 操作。
(2)行缓存:当输入或输出中遇到新行符时,标准 I/O 库执行 I/O 操作。
(3)不带缓存:stderr 就是了。( stderr:Linux/unix 标准输出(设备)文件,对应终端的屏幕。进程将从标准输入文件中得到输入数据,将正常输出数据输出到标准输出文件,而将错误信息送到标准错误文件中。在 C 中,程序执行时,一直处于开启状态。)
二、文件 I/O 和标准 I/O的区别:
文件 I/O 又称为低级磁盘 I/O,遵循 POSIX 相关标准。任何兼容 POSIX 标准的操作系统上都支持文件 I/O。
标准 I/O 被称为高级磁盘 I/O,遵循 ANSI C 相关标准。只要开发环境中有标准 I/O 库,标准 I/O 就可以使用。
Linux 中使用的是 GLIBC,它是标准 C 库的超集。不仅包含 ANSI C 中定义的函数,还包括 POSIX 标准中定义的函数。因此,Linux 下既可以使用标准I/O,也可以使用文件 I/O。
通过文件 I/O 读写文件时,每次操作都会执行相关系统调用。这样处理的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销。
标准 I/O 可以看成是在文件 I/O 的基础上封装了缓冲机制。先读写缓冲区,必要时再访问实际文件,从而减少了系统调用的次数。
文件 I/O 中用文件描述符表现一个打开的文件,可以访问不同类型的文件如普通文件、设备文件和管道文件等。
而标准 I/O 中用 FILE(流)表示一个打开的文件,通常只用来访问普通文件。
三、C 标准函数与系统函数的区别
带缓冲区的 c 标准函数:
1. I/O 缓冲区
每一个 FILE 文件流都有一个缓冲区 buffer,默认大小 8192 Byte。
2. 效率
这个要看情况才能决定那个效率高,不同情况,效率是不一样的。
3. 程序的跨平台性
事实上 Unbuffered I/O 这个名词是有些误导的,虽然 write 系统调用位于 C 标准库 I/O 缓冲区的底层,但在 write 的底层也可以分配一个内核 I/O 缓冲区,所以 write 也不一定是直接写到文件的,也可能写到内核 I/O 缓冲区中,至于究竟写到了文件中还是内核缓冲区中对于进程来说是没有差别的,如果进程 A 和进程 B 打开同一文件,进程 A 写到内核 I/O 缓冲区中的数据从进程 B 也能读到,而 C 标准库的 I/O 缓冲区则不具有这一特性。
四、PCB(进程控制块 Process Control Block)概念
为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB Process Control Block),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个 PCB,在创建进程时,建立 PCB,伴随进程运行的全过程,直到进程撤消而撤消。
PCB 进程控制块是进程的静态描述,由 PCB、有关程序段和该程序段对其进行操作的数据结构集三部分组成。
在 Unix 或 Linux 系统中,进程是由进程控制块,进程执行的程序,进程执行时所用数据,进程运行使用的工作区组成。其中进程控制块是最重要的一部分。
进程控制块是用来描述进程的当前状态,本身特性的数据结构,是进程中组成的最关键部分,其中含有描述进程信息和控制信息,是进程的集中特性反映,是操作系统对进程具体进行识别和控制的依据。PCB 一般包括:
1. 程序 ID(PID、进程句柄):它是唯一的,一个进程都必须对应一个 PID。PID 一般是整形数字
2. 特征信息:一般分系统进程、用户进程、或者内核进程等
3. 进程状态:运行、就绪、阻塞,表示进程现的运行情况
4. 优先级:表示获得 CPU 控制权的优先级大小
5. 通信信息:进程之间的通信关系的反映,由于操作系统会提供通信信道
6. 现场保护区:保护阻塞的进程用
7. 资源需求、分配控制信息
8. 进程实体信息,指明程序路径和名称,进程数据在物理内存还是在交换分区(分页)中
9. 其他信息:工作单位,工作区,文件信息等
文件的通用操作方法
本篇博客会介绍文件的通用操作方法。先介绍如何建立文件、打开文件、读取和写入数据, 以及一些常用的文件控制函数,包括 stat()、fctnl() 和 ioctl()。所举例子大多数指的是磁盘中的文件操作,但是其操作方法并不限于此,对设备文件同样有效。
一、文件描述符
在 Linux 下用文件描述符来表示设备文件和普通文件。文件描述符是一个整型的数据,所有对文件的操作都通过文件描述符实现。
文件描述符是文件系统中连接用户空间和内核空间的枢纽。当打开一个或者创建一个文件时,内核空间创建相应的结构,并生成一个整型的变量传递给用户空间的对应进程。进程用这个文件描述符来对文件进行操作。用户空间的文件操作,例如读或写一个文件时,将文件描述符作为参数传送给 read 或 write。读写函数的系统调用到达内核时,内核解析作为文件描述符的整型变量,找出对应的设备文件运行相应的函数。并返回用户空间结果。
文件描述符的范围是 0〜OPEN_MAX,因此是一个有限的资源,在使用完毕后要及时释放,通常是调用 close() 函数关闭。文件描述符的值仅在同一个进程中有效,即不同进程的文件描述符,同一个值很可能描述的不是同一个设备或者普通文件。
在 Linux 系统中有 3 个己经分配的文件描述符,即标准输入、标准输出和标准错误, 它们文件描述符的值分别为 0、1 和 2。小伙伴可以査看 /dev/ 下的 stdin (标准输入)、stdout(标准输出)和 stderr(标准错误),会发现分別指向了 /proc/self/fd/ 目录下的 0、1、2 文件。
二、打开创建文件 open()、create() 函数
在 Linux 下,open() 函数用于打开一个已经存在的文件或者创建一个新文件,create() 函数用于创建一个新文件。
1. 函数 open()、create() 介绍
这两个函数的原型如下,根据用户设置的标志 flags 和模式 mode 在路径 pathname 下建立或者打开一个文件。
int open(const char *pathname, int flags); //pathname 你要打开的文件名
int open(const char *pathname, int flags, mode_t mode);
返回值:成功返回新分配的文件描述符,出错返回 -1 并设置 errno
在使用这些函数的时候,需要包含头文件 sys/types.h、sys/stat.h 和 fcntl.h。open() 函数打开 pathname 指定的文件,当函数成功时,返回一个整型的文件描述符。 这个函数正常情况下会返回一个文件描述符的值,在出错的时候会返回 -1。
打开文件的时候需要指定打开的文件路径,这个参数由 pathname 指定。函数会根据这个参数的值在路径中查找文件并试图打开或者建立文件。pathname 所指的为一个字符串变量,这个变量的长度在不同的系统下其最大长度有差别,通常情况下为 1024 个字节。当所给的路径长度大于这个数值的时候,系统会对字符串进行截断,仅选择最前面的字节进行操作。
文件的打开标志 flags 用于设置文件打开后允许的操作方式,可以为只读、只写或读写。 分别用 O_RDONLY (只读)、O_WRONLY (只写) 和 O_RDWR (读写) 表示。在打开文件的时候必须指定上述的三种模式之一。三个参数中 O_RDONLY 通常定义为 0,O_WRONLY 定义为 1,O_RDWR 定义为 2。
O_RDONLY (只读)、O_WRONLY (只写) 和 O_RDWR (读写) 为必选项。
参数 flags 除了上述三个选项之外,还有一些可选的参数。以下可选项可以同时指定 0 个或多个,和必选项按位或(|)起来作为 flags 参数。可选项有很多,这里只介绍一部分,其它选项可参考 open(2) 的 Man Page:
█ O_APPEND 选项:使每次对文件进行写操作都追加到文件的尾端。
█ O_CREAT:如果文件不存在则创建它,当使用此选择项时,第三个参数 mode 需要同时设定,用来说明新文件的权限。
█ O_EXCL:查看文件是否存在。如果同时指定了 O_CREAT,而文件已经存在,会返回错误。用这种方法可以安全地打开一个文件。
█ O_NONBLOCK:对于设备文件,以 O_NONBLOCK 方式打开可以做非阻塞 I/O(Nonblock I/O)。
█ O_TRUNC:将文件长度截断为 0。如果此文件存在,并且文件以只写或可读可写方式成功打开,则会将其长度截断(Truncate)为 0。例如:
open (pathname, O_RDWRO_CREAT | O_TRUNC, mode);
通常使用 O_TRUNC 选项对需要清空的文件进行归零操作。O_NONBLOCK 打开文件为非阻塞方式,如果不指定此项,默认的打开方式为阻塞方式,即对文件的读写操作需要等待操作的返回状态。其中,参数 mode 用于表示打开文件的权限,mode 的使用必须结合 flags 的 O_CREAT 使用,否则是无效的。它们的值在下表(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 | 其他用户对文件有执行权限 |
注意 open 函数与 C 标准 I/O 库的 fopen 函数有些细微的区别:
以可写的方式 fopen 一个文件时,如果文件不存在会自动创建,而 open 一个文件时必须明确指定 O_CREAT 才会创建文件,否则文件不存在就出错返回。
以 w 或 w+ 方式 fopen 一个文件时,如果文件已存在就截断为 0 字节,而 open 一个文件时必须明确指定 O_TRUNC 才会截断文件,否则直接在原来的数据上改写。
第三个参数 mode 指定文件权限,可以用八进制数表示,比如 0644 表示 -rw-r-r–,也可以用S_IRUSR、S_IWUSR 等宏定义按位或起来表示,详见 open(2) 的 Man Page。要注意的是,文件权限由 open 的 mode 参数和当前进程的 umask 掩码共同决定。
用 touch 命令创建一个文件时,创建权限是 0666,而 touch 进程继承了 Shell 进程的 umask 掩码,所以最终的文件权限是 0666&~022=0644。
$ touch file123
$ ls -l file123
-rw-rw-r-- 1 chenshunyi chenshunyi 0 9月11 23:48 file123
同样道理,用 gcc 编译生成一个可执行文件时,创建权限是 0777,而最终的文件权限是:
0777 &~ 022 = 0755。
chenshunyi@ubuntu:~$ umask
0002
chenshunyi@ubuntu:~$ gcc main.c
chenshunyi@ubuntu:~$ ls -l a.out
-rwxrwxr-x 1 chenshunyi chenshunyi 7158 9月11 23:51 a.out
2. 使用函数 open() 的例子
这个例子为在当前目录下打幵一个文件名为 test.txt 的文件,并根据文件是否成功打开打印输出不同的结果。程序的代码如下:
/* open_01.c 打开文件的例子 */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
int fd = -1; /* 文件描述符声明 */
char filename[] = "test.txt"; /* 打开的文件名 */
fd = open(filename,O_RDWR); /* 打开文件为可读写方式 */
if(-1 == fd) /* 打开失败 */
{
printf("Open file %s failure!,fd:%d\n",filename,fd);
}
else /* 打开成功 */
{
printf("Open file %s success,fd:%d\n",filename,fd);
}
return 0;
}
将上述代码保存到文件 open_01.c 中,按照如下的命令进行编译:
$gcc -o open_01 open_01.c
运行编译出来的可执行文件 open_01,会发现第一次执行失败:
$ ./open_01
Open file test.txt failure!, fd:-1
因为此时当前目录下没有文件 test.txt,所以打开文件会失败。建立一个空的 test.txt 文件:
$echo "">test.txt
再次运行程序:
$ ./open_01
Open file test.txt success,fd:3
这次打开文件成功了,返回的文件描述符的值为 3。在 Linux 下如果之前没有其他文件打开,第一个调用打开文件成功的程序,返回的描述符为最低值,即 3。因为 0、1、2 文件描述符分配给了系统,表示标准输入(描述符 0)、标准输出(描述符 1)和标准错误(描述符 2)。在 Linux 下可以直接对这 3 个描述符进行操作(例如读写),而不用打开、关闭。
open() 函数不仅可以打开一般的文件,而且可以打开设备文件,例如 open() 函数可以打开设备 “/dev/sda1”,即磁盘的第一个分区,将文件 open_01.c 中打开的文件名修改为:
char filename[] = "/dev/sda1";
保存为文件名 open_02.c,重新编译后运行,结果为:
$ gcc -o open_02 open_02.c
$ ./open_02
Open file /dev/sda1 success,fd:3
O_CREAT 可以创建文件,与 O_EXCL 结合使用可以编写容错的程序,例如将 “open_01.c” 修改为如下代码:
/*文件 open_03.c, O_CREAT 和 O_EXCL 的使用 */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
int fd = -1;
char filename[] = "test.txt"; /* 打开文件,如果文件不存在,则报错 */
fd = open(filename,O_RDWR|O_CREAT|O_EXCL,S_IRWXU);
if(-1 == fd) /* 文件已经存在 */
{
printf("File %s exist! reopen it,",filename);
fd = open (filename ,O_RDWR) ; /* 重新打开 */
printf("fd:%d\n",fd);
}
else /* 文件不存在,创建并打开 */
{
printf("Open file %s success,fd:%d\n",filename,fd);
}
return 0;
}
将代码保存为 open_03.c,编译文件:
$gcc -o open_03 open_03.c
当文件 test.txt 存在时运行的结果为:
$./ open_03
File test.txt exist!reopen it,fd:3
删除 test.txt 后再次运行 open_03,结果为:
$rm -f test.txt
$./ open_03
Open file test.txt success,fd:3
査看当前目录下的文件,会发现多了一个文件大小为 0 的文件 test.txt。
创建文件的函数除了可以在打开时创建外,还可以使用 create() 函数创建一个新文件,其函数的原型如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
函数 creat 等于一个 open 的缩写版本,等效于如下方式的 open。
open(pathname, O_WRONLY, O_CREAT, O_TRUNC, mode);
creat 的返回值与 open 一样,在成功时为创建文件的描述符。
三、关闭文件 close() 函数
close() 函数关闭一个打开的文件,之前打开文件所占用的资源。
1. close() 函数介绍
close() 函数的原型如下:
#include <unistd.h>
int close(int fd);
返回值:成功返回 0,出错返回-1 并设置 errno
close() 函数关闭一个文件描述符,关闭以后此文件描述符不再指向任何文件,从而描述符可以再次使用。当函数执行成功的时候返回 0,如果有错误发生,例如文件描述符非法,返回 -1。在使用这个函数的时候,通常不检查返回值。
在打开文件之后,必须关闭文件。如果一个进程中没有正常关闭文件,在进程退出的时候系统会自动关闭打开的文件。但是打开一个文件的时候,系统分配的文件描述符为当前进程中最小的文件描述符的值,这个值般情况下是递增的,而每个进程中的文件描述符的数量是有大小限制的。如果一个进程中频繁地打开文件而又忘记关闭文件,当系统的文件描述符达到最大限制的时候,就会因为没有文件描述符可以分配造成打开文件失败。
参数 fd 是要关闭的文件描述符。需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用 close 关闭,所以即使用户程序不调用 close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。
由 open 返回的文件描述符一定是该进程尚未使用的最小描述符。由于程序启动时自动打开文件描述符 0、1、2,因此第一次调用 open 打开文件通常会返回描述符 3,再调用open就会返回 4。可以利用这一点在标准输入、标准输出或标准错误输出上打开一个新文件,实现重定向的功能。例如,首先调用 close 关闭文件描述符 1,然后调用 open 打开一个常规文件,则一定会返回文件描述符 1,这时候标准输出就不再是终端,而是一个常规文件了,再调用 printf 就不会打印到屏幕上,而是写到这个文件中了。
2. close() 函数的例子
下面的代码用于打开当前目录下的 test.txt 文件,每次打开后并不关闭,一直到系统出现错误为止。这个程序用于测试与前系统文件描述符的最大支持数量,代码如下:
int main()
{
int i=0; /* 计数器 */
int fd=0; /* 文件描述符 */
for (i=0; fd>=0; i++) /* 循环打开文件直到出错 */
{
fd = open("test.txt",O_RDONLY); /* 只读打开文件 */
if(fd > 0) /* 打开文件成功 */
{
printf("fd:%d\n",fd); /* 打印文件描述符 */
}
else /* 打开文件失败 */
{
printf("error, can't open file\n"); /* 打印错误 */
exit(0); /* 退出 */
}
}
}
要测试这个文件需要在当前目录下建立一个 test.txt 的文件,可以使用 “echo"">test.txt” 来建立。执行程序后的文件内容为:
fd:3
fd:4
......
fd:1022
fd:1023
error, can't open file
系统打开第一个文件的文件描述符的值为 3,一直到文件描述符的值为 1023 都可以正常打开。但是由于程序中一直没有关闭文件,到文件描述符为 1024 的时候,由于超过了系统的可分配最大值,发生了错误。小伙伴可以修改上述的代码,加入 close() 函数调用后,会发现程序可以正常地运行。
最大打开文件个数
查看当前系统允许打开最大文件个数
$ cat /proc/sys/fs/file-max
194747
当前默认设置最大打开文件个数 1024
$ ulimit -a
......
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
......
修改默认设置最大打开文件个数为 4096
$ ulimit -n 4096
四、读取文件 read() 函数
用 read() 函数从打开文件中读数据,用户可以对读入的数据进行操作。
1. read() 函数介绍
使用这个函数需要将头文件 unistd.h 加入。read() 函数从文件描述符 fd 对应的文件中读取 count 字节,放到 buf 开始的缓冲区。如果 count 的值为 0,read() 函数返回 0,不进行其他操作:如果 count 的值大于 SSIZE_MAX,结果不可预料。在读取成功的时候,文件对应的读取位置指针,向后移动位置,大小为成功读取的字节数。
read() 函数的原型定义格式如下。
ssize_t read(int fd, void *buf, size_t count);
如果 read() 函数执行成功,返回读取的字节数;当返回值为 -1 的时候,读取函数有错误发生。如果已经到达文件的末尾,返回 0。返回值的数据类型为 ssize_t 这是一个可能不同于 int、long 类型的数据类型,它是一个符号数,具体实现时可能定义为 long 或者 int 。
read() 函数的参数 fd 是一个文件描述符,通常是 open() 函数或者 creat() 函数成功返回的值;参数 buf 是一个指针,它指向缓冲区地址的开始位置,读入的数据将保存在这个缓冲区中;参数 count,表示要读取的字节数量,通常用这个变量来表示缓冲区的大小,因此 count 的值不要超过缓冲区的大小,否则很容易造成缓冲区的溢出。
在使用 read() 函数时,count 为请求读取的字节数量,但是 read() 函数不一定能够读取这么多数据,有多种情况可使实际读到的字节数小于请求读取的字节数。
█ 读取普通文件时,文件中剩余的字节数不够请求的字节数,例如在文件中剩余了 10 个字节,而 read() 函数请求读取 80 个字节,这时 read() 函数会将剩余的 10 个字节数写到缓冲区 buf 中,并返回实际读到的字节数 10,下次 read 将返回 0。
█ 当从终端设备读取数据的时候,通常以行为单位,读到换行符就返回了。其默认的长度不够 read() 函数请求读取的数据,例如终端缓冲区的大小为 256 字节,而 read() 函数请求读取 1024 个字节。
█ 当从网络读取数据时,根据不同的传输层协议和内核缓存机制,缓冲区大小可能小于读取请求的数据大小。
因此读取数据时,要判断返回实际读取数据大小来进行处理。
2. read() 函数的例子
下面的代码从文本文件 test.txt 中读取数据,文件中存放的是字符串 quick brown fox jumps over the lazy dog。读取成功后将数据打印出来。
/* 文件 read_01.c, O_CREAT 和 O_EXCL 的使用 */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int fd = -1;
int i;
ssize_t size = -1;
char buf[10]; /* 存放数据的缓冲区 */
char filename[] = "test.txt";
fd = open(filename,O_RDONLY) ; /* 打开文件,如果文件不存在,则报错 */
if(-1 == fd) /* 文件已经存在 */
{
printf("Open file %s failure, fd:%d\n", filename, fd);
}
else /* 文件不存在,创建并打开 */
{
printf("Open file %s success, fd:%d\n", filename, fd);
}
/* 循环读取数据,直到文件末尾或者出错 */
while(size)
{
size = read (fd, buf, 10); /* 每次读取 10 个字节数据 */
if( -1 == size) /* 读取数据出错 */
{
close(fd); /* 关闭文件 */
printf("read file error occurs\n");
return -1; /* 返回 */
}
else /* 读取数据成功 */
{
if(size >0 )
{
printf("read %d bytes:",size); /* 获得 size 个字节数据 */
printf("\""); /* 打印引号 */
for(i - 0;i<size;i++) /* 将读取的数据打印出来 */
{
printf("%c",*(buf+i));
}
printf("\"\n"); /* 打印引号并换行 */
}
else
{
printf("reach the end of file\n");
}
}
}
return 0;
}
将上述代码保存到文件 read_01.c 中,编译文件,并运行成功编译后生成的可执行文件 read。
$gcc -o read_01 read_01.c
$./read_01
Open file test.txt success,fd:3 (打开文件成功,文件描述符为 3 )
read 10 bytes:"quick brow" (读取了 10 个字节数据:"quick brow")
read 10 bytes:"n fox jump" (读取了 10 个字节数据:"n fox jump")
read 10 bytes:"s over the" (读取了 10 个字节数椐:"s over the")
read 9 bytes:" lazy dog" (读取了 9 个字节数:" lazy dog")
reach the end of file (到达文件末尾)
五、写文件 write() 函数
write() 函数向打开的文件中写入数据,将用户的数据保存到文件中。写常规文件时, write 的返回值通常等于请求写的字节数 count,而向终端设备或网络写则不一定。
1. write() 函数介绍
与 read() 函数的含义相似,write() 函数向文件描述符 fd 写入数据,数据的大小由 count 指定,buf 为要写入数据的指针,write() 函数返回值为成功写入数据的字节数。当操作的对象是普通文件时,写文件的位置从文件的当前开始,操作成功后,写的位罝会增加写入字节数的值。如果在打开文件的时候指定了 O_APPEND 项,每次写操作之前,会将写操作的位置移到文件的结尾处。write() 函数的原型如下。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
函数操作成功会返回写入的字节数,当出错的时候返回 -1。出错的原因有多种,像磁盘己满,或者文件大小超出系统的设置,例如 ext2 下的文件大小限制为 2Gbytes 等。
写操作的返回值与想写入的字节数会存在差异,与 read() 函数的原因类似。
㊨ 注意:写操作函数并不能保证将数据成功地写入磁盘,这在异步操作中经常出现,write() 函数通常将数据写入缓冲区,在合适的时机由系统写入实际的设备。可以调用 fsync() 函数,显示将输入写入设备。
2. write() 函数的例子
假设在磁盘上存在一个大小为 50 字节的文件 test.txt,向其中写入数据 quick brown fox jumps over the lazy dog,在写入前后的文件大小不变,只是文件开始部分的内容改变了。
/* 文件 write_01.c, write() 函数的使用 */
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
int fd = -1;
char buf[]="quick brown fox jumps over the lazy dog"; /* 存放数据的缓冲区 */
char filename[] = "test.txt”;
fd = open (filename,O_RDWR); /* 打开文件,如果文件不存在,则报错 */
if (-1 == fd) /* 文件己经存在 */
{
printf("Open file %s failure,fd:%d\n",filename,fd);
}
else /* 文件不存在,创建并打开 */
{
printf("Open file %s success,fd:%d\n",filename,fd);
}
size = write(fd, buf,strlen(buf)); /*将数据写入到文件 test.txt 中 */
printf("write %d bytes to file %s\n",size,filename);
close(fd); /* 关闭文件 */
return 0;
}
将此代码存入 write_01.c 后编译,运行代码并查看文件大小,会发现文件 test.txt 的大小没有改变但文件的内容发生了变化:
$ ls -l test.txt
-rw-rw-r-- 1 linux-c linux-c 51 5 月 31 15:11 test.txt
$ cat test.txt
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
$gcc -o write write_01.c
$./write
Open file test.txt success,fd:3
write 39 bytes to file test.txt
$ ls -l test.txt
-rw-rw-r-- 1 linux-c linux-c 51 5 月 31 15:12 test.txt
$ cat test.txt
quick brown fox jumps over the lazy dogaaaaaaaaaaa
写入的 39 个字符仅仅覆盖了文件 test.txt 开头的部分。要在写入的时候对文件进行清空,可以使用 open() 函数的 O_TRUNC 选项,将打开的函数修改为如下形式:
fd = open(filename,O_RDWR | O_TRUNC);
编译后再次运行会发现这次写入后文件的大小变为 39 了。
六、阻塞和非阻塞
读常规文件是不会阻塞的,不管读多少字节,read 一定会在有限的时间内返回。
从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用 read 读终端设备就会阻塞,如果网络上没有接收到数据包,调用 read 从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。
现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用 sleep 指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在 Linux 内核中,处于运行状态的进程分为两种情况:
正在被调度执行。CPU 处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但 CPU 暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。下面这个小程序从终端读数据再写回终端。
1. 阻塞读终端
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
char buf[10];
int n;
n = read(STDIN_FILENO, buf, 10);
if (n < 0) {
perror("read STDIN_FILENO");
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
执行结果如下:
$ ./a.out
hello(回车)
hello
$ ./a.out
hello world(回车)
hello worl$ d
bash: d: command not found
第一次执行 a.out 的结果很正常,而第二次执行的过程有点特殊,现在分析一下:
Shell 进程创建 a.out 进程,a.out 进程开始执行,而 Shell 进程睡眠等待 a.out 进程退出。
a.out 调用 read 时睡眠等待,直到终端设备输入了换行符才从 read 返回,read 只读走 10 个字符,剩下的字符仍然保存在内核的终端设备输入缓冲区中。
a.out 进程打印并退出,这时 Shell 进程恢复运行,Shell 继续从终端读取用户输入的命令,于是读走了终端设备输入缓冲区中剩下的字符 d 和换行符,把它当成一条命令解释执行,结果发现执行不了,没有 d 这个命令。
如果在 open 一个设备时指定了 O_NONBLOCK 标志,read/write 就不会阻塞。以 read 为例,如果设备暂时没有数据可读就返回 -1,同时置 errno 为 EWOULDBLOCK(或者 EAGAIN,这两个宏定义的值相同),表示本来应该阻塞在这里( would block,虚拟语气),事实上并没有阻塞而是直接返回错误,调用者应该试着再读一次( again)。这种行为方式称为轮询(Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备:
while(1) {
非阻塞read(设备1);
if(设备1有数据到达)
处理数据;
非阻塞read(设备2);
if(设备2有数据到达)
处理数据;
...
}
使用阻塞 I/O 的话,如果 read(设备 1)是阻塞的,那么只要设备 1 没有数据到达就会一直阻塞在设备 1 的 read 调用上,即使设备 2 有数据到达也不能处理,使用非阻塞 I/O 就可以避免设备 2 得不到及时处理。
非阻塞 I/O 有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了。在使用非阻塞 I/O 时,通常不会在一个 while 循环中一直不停地查询(这称为 Tight Loop),而是每延迟等待一会儿来查询一下,以免做太多无用功,在延迟等待的时候可以调度其它进程执行。
while(1) {
非阻塞read(设备1);
if(设备1有数据到达)
处理数据;
非阻塞read(设备2);
if(设备2有数据到达)
处理数据;
...
sleep(n);
}
这样做的问题是,设备 1 有数据到达时可能不能及时处理,最长需延迟 n 秒才能处理,而且反复查询还是做了很多无用功。以后要学习的 select(2) 函数可以阻塞地同时监视多个设备,还可以设定阻塞等待的超时时间,从而圆满地解决了这个问题。
以下是一个非阻塞 I/O 的例子。目前我们学过的可能引起阻塞的设备只有终端,所以我们用终端来做这个实验。程序开始执行时在 0、1、2 文件描述符上自动打开的文件就是终端,但是没有 O_NONBLOCK 标志。所以就像上例 “阻塞读终端”一样,读标准输入是阻塞的。我们可以重新打开一遍设备文件 /dev/tty(表示当前终端),在打开时指定 O_NONBLOCK 标志。
2. 非阻塞读终端
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#define MSG_TRY "try again\n"
int main(void)
{
char buf[10];
int fd, n;
fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
if(fd<0) {
perror("open /dev/tty");
exit(1);
}
tryagain:
n = read(fd, buf, 10);
if (n < 0) {
if (errno == EAGAIN) {
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
perror("read /dev/tty");
exit(1);
}
write(STDOUT_FILENO, buf, n);
close(fd);
return 0;
}
以下是用非阻塞 I/O 实现等待超时的例子。既保证了超时退出的逻辑又保证了有数据到达时处理延迟较小。
3. 非阻塞读终端和等待超时
如果一直等待输入也不行,那样就相当于成为了一个死循环了,所以一般情况下,会有一个等待超时的设置,类似于大家在点开某个网页时,如果网络不好的话,它会转一会圈圈然后就显示连接超时,这个原理和那个类似,下列代码设置成循环五次,如果还没有输入数据就停止等待输入了。
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "timeout\n"
int main(void)
{
char buf[10];
int fd, n, i;
fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
if(fd<0) {
perror("open /dev/tty");
exit(1);
}
for(i=0; i<5; i++) {
n = read(fd, buf, 10);
if(n>=0)
break;
if(errno!=EAGAIN) {
perror("read /dev/tty");
exit(1);
}
sleep(1);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
}
if(i==5)
write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
else
write(STDOUT_FILENO, buf, n);
close(fd);
return 0;
}
七、文件偏移量 lseek() 函数
在调用 read() 和 write() 函数时,每次操作成功后,文件当前的操作位置都会移动。其中隐含了一个概念,即文件的偏移量。
文件的偏移量指的是当前文件操作位置相对于文件开始位置的偏移。
每次打开和对文件进行读写操作后,文件的偏移量都进行了更新。当写入数据成功时,文件的偏移量要向后移动写入数据的大小。当从文件中读出数据的时候,文件的偏移量要向后移动读出数据的大小。
文件的偏移量是一个非负整数,表示从文件的开始到当前位置的字节数。一般情况下,对文件的读写操作都从当前的文件位移量处开始,并增加读写操作成功的字节数。当打开一个文件时,如果没有指定 O_APPEND 选择项,文件的位移量为 0。如果指定了 O_APPEND 选项,文件的偏移量与文件的长度相等,即文件的当前操作位置移到了末尾。
1. Iseek() 函数介绍
lseek() 函数可以设置文件偏移量的位置,lseek() 的函数原型如下:
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fildes, off_t offset, int whence);
这个函数对文件描述符 fildes 所代表的文件,按照操作模式 whence 和偏移的大小 offset,重新设定文件的偏移量。
如果 lseek() 函数操作成功,就返回新的文件偏移量的值;如果失败,则返回 -1。由于文件的偏移量可以为负值,判断 lseek() 是否操作成功时,不要使用小于 0 的判断,要使用是否等于 -1 来判断函数失败。
参数 whence 和 offset 结合使用。whence 表示操作的模式,offset 是偏移的值,和fseek一样,偏移量允许超过文件末尾,这种情况下对该文件的下一次写操作将延长文件,中间空洞的部分读出来都是0。offset 的值可以为负值。offset 值的含义如下:
█ 如果 whence 为 SEEK_SET,则 offset 为相对文件开始处的值,即将该文件偏移量设为距文件开始处 offset 个字节。
█ 如果 whence 为 SEEK_CUR,则 offset 为相对当前位置的值,即将该文件的偏移量设置为其当前值加 offset。
█ 如果 whence 为 SEEK_END,则 offset 为相对文件结尾的值,即将该文件的偏移量设置为文件长度加 offset。
函数 lseek 执行成功时返回文件的偏移量,可以用 SEEK_CUR 模式下偏移 0 的方式获得当前的偏移量,例如:
off_t cur_pos = lseek(fd, 0, SEEK_CUR);
上面的函数没有引起文件的副作用,仅仅检验了文件的偏移设置函数获得当前的文件偏移量的值,可以用这种方法测试当前的文件或设备是否可以设置偏移量,常规文件都可以设置偏移量,而设备一般是不可以设置偏移量的。如果设备不支持 lseek,则 lseek 返回 -1,并将 errno 设置为 ESPIPE。注意 fseek 和 lseek 在返回值上有细微的差别,fseek 成功时返回 0 失败时返回 -1,要返回当前偏移量需调用 ftell,而 lseek 成功时返回当前偏移量失败时返回 -1。lseek 可以用来拓展文件,拓展文件的时候,一定要有一次写操作。
2. Iseek() 函数的通用例子
下面的代码测试标准输入是否支持 lseek() 操作。程序中对标准输入(stdin)进行偏移操作(lseek),根据系统的返回值判断是否可以对标准输入进行偏移操作。
/*文件 lseek-01.c,使用 lseek 函数测试标准输入是否可以进行 seek 操作 */
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
off_t offset = -1;
offset = lseek(1, 0, SEEK_CUR) ; /* 将标准输入文件描述符的文件偏移量设为当前值 */
if(-1 == offset) /* 设置失败,标准输入不能进行 seek 操作 */
{
printf("STDIN can't seek\n");
return -1;
}
else /* 设置成功,标准输入可以进行 seek 操作 */
{
printf("STDIN CAN seek\n");
}
return 0;
}
上面代码中的 1 是标准输入(stdin)的文件描述符。编译执行此代码可知,标准输入不能进行 Iseek() 操作。
$gcc -o lseek-01 lseek-01.c
$ ./lseek-01
STDIN can't seek
3. 空洞文件的例子
lseek() 函数对文件偏移量的设置可以移出文件,即设置的位置可以超出文件的大小, 但是这个位置仅仅在内核中保存,并不引起任何的 IO 操作。当下一次的读写动作时,lseek() 设置的位置就是操作的当前位置。当对文件进行写操作时会延长文件,跳过的数据用 ”\0” 填充,这在文件中造成了一个空洞。
例如建立一个文件在开始的部分写入 8 个字节 “01234567”,然后到 32 的地方在写入 8 个不同的字节 ABCDEFGH,文件会形成下表(空洞文件的内容)所示的情况。下面的代码是造成上述情况文件空洞的一个实例。
00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 | 13 | 14 | 15 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0000000 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | \0 | \0 | \0 | \0 | \0 | \0 | \0 | \0 |
0000020 | \0 | \0 | \0 | \0 | \0 | \0 | \0 | \0 | \0 | \0 | \0 | \0 | \0 | \0 | \0 | \0 |
0000040 | A | B | C | D | E | F | G | H |
/* 文件 lseek_02.c,使用 lseek() 函数构建空洞文件 */
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
int fd = -1;
ssize_t size = -1;
off_t offset = -1;
char buf1[]="01234567"; /* 存放数据的缓冲区*/
char buf2[]="ABCDEFGH";
char filename[] = "hole.txt"; /* 文件名 */
int len = 8;
fd = open(filename,O_RDWR|O_CREAT,S_IRWXU); /* 创建文件 hole. txt */
if (-1 == fd)
{
return -1; /* 创建文件失败 */
}
size = write(fd, buf1,len); /* 将 buf1 中的数据写入到文件 hole.txt 中 */
if (size != len) /* 写入数据失败 */
{
return -1;
}
offset = lseek(fd, 32, SEEK_SET); /* 设置文件偏移量为绝对值的 32 */
if (-1 == offset) /* 设置失败 */
{
return -1;
}
size = write(fd, buf2,len); /* 将 buf2 中的数据写入到文件 hole.txt 中 */
if (size != len) /* 写入数据失败 */
{
return -1;
}
close(fd); /* 关闭文件 */
return 0;
}
将代码保存到文件 lseek-02.c 中并编译运行:
$gcc -o lseek-02 lseek-02.c
./lseek-02
生成了 hole.txt 文件,文件的大小为 40 字节。用十六进制工具 od 查看生成的 hole.txt 文件内容:
$ od -c hole.txt
0000000 0 1 2 3 4 5 6 7 \0 \0 \0 \0 \0 \0 \0 \0
0000020 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
0000040 A B C D E F G H
0000050
0〜7 的位置用 buf1 中的内容填充:中间的 24 的未写字节即在 8〜32 个字节的位置上均为 “\0”,即进行偏移造成的文件空洞(33〜39)是 buf2 的内容。
八、获得文件状态 fstat() 函数
有的时候对文件操作的目的不是读写文件,而是要获得文件的状态。例如,获得目标文件的大小、权限、时间等信息。
1. fstat() 函数介绍
在程序设计的时候经常要用到文件的一些特性值,例如文件的所有者、文件的修改时间、文件的大小等。stat() 函数、fstat() 函数和 lstat() 函数可以获得文件的状态,其函数原型如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf);
int fstat(int filedes, struct stat *buf);
int lstat(const char *path, struct stat *buf);
函数的第一个参数是文件描述的参数,可以为文件的路径或者文件描述符;buf 为指向 struct stat 的指针,获得的状态从这个参数中传回。当函数执行成功时返回 0,返回值为 -1 表示有错误发生。
结构 struct stat 为一个描述文件状态的结构,定义如下:
struct stat
{
dev_t st_dev; /* 此文件所处设备的设备 ID 号 */
ino_t st_ino; /* inode 数值 */
mode_t st_mode; /* 保护设置 */
nlink_t st_nlink; /* 硬链接数 */
uid_t st_uid; /* 文件所有者的 ID */
gid_t st_gid; /* 文件所有者的组 ID */
dev_t st_rdev; /* 以字节计的大小 */
blksize_t st_blksize /* 文件系统的块大小 */
blkcnt_t st_blocks; /* 占用的块的数量 */
time_t st_atime; /* 最后方位时间 */
time_t st_mtime; /* 最后修改时间 */
time_t st_ctime; /* 最后状态改变时间 */
};
2. stat() 函数的例子
下面的代码为获得文件状态的实例,获得文件 test.txt 的状态并将状态值打印出来。
/* 文件 fstat_01.c,使用 stat 获得文件的状态 */
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
int main(void)
{
struct stat st;
if(-1 == stat("test.txt",&st)) /* 获得文件的状态,将状态值放入 st 中 */
{
printf("获得文件状态失败\n");
return -1;
}
printf("包含此文件的设备 ID: %d\n",st.st_dev); /* 文件的 ID 号 */
printf("此文件的节点:%d\n",st.st_ino); /* 文件的节点 */
printf("此文件的保护模式:%d\n",st.st_mode); /* 文件的模式 */
printf("此文件的硬链接数:%d\n",st.st_nlink); /* 文件的硬链接数 */
printf("此文件的所有者ID:%d\n", st.st_uid); /* 文件的所有者 ID */
printf("此文件的所有者的组ID: %d\n",st.st_gid); /* 文件的组ID */
printf("设备 ID (如果此文件为特殊设备):%d\n",st.st_redev); /* 文件的设备ID */
printf("此文件的大小:%d\n",st.st_size); /* 文件的大小 */
printf("此文件的所在文件系统块大小:%d\n",st.st_blksize); /* 文件的系统块大小 */
printf("此文件的占用块数量:%d\n", st.st_blocks); /* 文件的块大小 */
printf("此文件的最后访问时间:%d\n",st.st_atime); /* 文件的最后访问时间 */
printf("此文件的最后修改时间:%d\n",st.st_mtime); /* 文件的最后修改时向 */
printf("此文件的最后状态改变时间:%d\n",st.st_ctime); /* 文件的最后状态改变时间 */
return 0;
}
将代码保存在 fstat_01.c 文件中,编译并运行:
$gcc -o fstat-01 fstat-01.c
$./fstat-01
包含此文件的设备ID: 2049
此文件的节点:1058718
此文件的保护模式:33204
此文件的硬链接数:1
此文件的所有者ID: 1000
此文件的所有者的组ID: 1000
设备ID(如果此文件为特殊设备):0
此文件的大小:51
此文件的所在文件系统块大小:4096
此文件的占用块数量:8
此文件的最后访问时间:1369985914
此文件的最后修改时间:1369984366
此文件的最后状态改变时间:1369985914
从上面的打印可知道,当前系统中每个块的大小为 4096 字节,文件 test.txt 大小为 50 字节,占用了一个块。
九、文件空间映射 mmap() 函数
mmap() 函数用来将文件或者设备空间映射到内存中,可以通过对映射后的内存空间存取来获得与存取文件一致的控制方式,不必再使用 read() 函数、write() 函数。简单地说,此函数就是将文件映射到内存中的某一段,内存比磁盘快些。映射到的内存并不占用空间,仅仅占用一段地址空间。
1. mmap() 函数介绍
mmap() 函数的原型如下,它将文件描述符 fd 对应的文件中,自 offset 开始的一段长 length 的数据空间映射到内存中。用户可以设定映射内存的地址,但是具体函数会映射到内存的位罝由返回值确定。当映射成功后,返回映射到的内存地址。如果失败返回值为 (void*)-1。通过 errno 值可以获得错误方式。
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
mmap() 函数进行地址映射的时候,用户可以指定要映射到的地址,这个地址在参数 start 中指定,通常为 NULL,表示由系统自己决定映射到什么地址。而参数 length 表示映射数据的长度,即文件需要映射到内存中的数据大小。使用 mmap() 函数有一个限制,只能对映射到内存的数据进行操作,即限制于开始为 offset、大小为 len 的区域。参数 fd,代表文件的文件描述符,表示要映射到内存中的文件,通常是 open() 函数的返回值;如果需要对文件中需要映射地址进行偏移,则在参数 offset 中进行指定。
mmap() 函数的参数 prot,表示映射区保护方式。保护方式 prot 的值是一个组合值,可选如下的一个或者多个。这些值可以进行复合运算,其中,PROT_EXEC 表示映射区域可执行,PROT_READ 表示映射区域可读取,PROT_WRITE 表示映射区域可写入, PROT_NONE 表示映射区域不能存取。例如 PROT_WRITE | PROT_READ 的方式将映射区设置为可读写,当然 prot 的设置受文件打开时的选项限制,当打开文件时为只读,则上面的写(PROT_WRITE)失效,但是读仍然有效。
参数 flags 用于设定映射对象的类型、选项和是否可以对映射对象进行操作(读写等),这个参数和 open() 函数中的含义类似。参数 flags 也是一个组合值,下面是其可选的设置。
█ MAP_FXED:如果参数 start 指定了用于需要映射到的地址,而所指的地址无法成功建立映射,则映射失败。通常不推荐使用此设置,而将 start 设为 0,由系统自动选取映射地址。
█ MAP_SHARED:共享的映射区域,映射区域允许其他进程共享,对映射区域写入数据将会写入到原来的文件中。
█ MAP_PRIVATE:当对映射区域进行写入操作时会产生一个映射文件的复制,即写入复制(copy on write),而读操作不会影响此复制。对此映射区的修改不会写回原来的文件,即不会影响原来文件的内容。
█ MAP_ANONYMOUS:建立匿名映射。此时会忽略参数 fd,不涉及文件,而且映射区域无法和其他进程共享。
█ MAP_DENYWRITE:对文件的写入操作将被禁止,只能通过对此映射区操作的方式实现对文件的操作,不允许直接对文件进行操作。
█ MAP_LOCKED:将映射区锁定,此区域不会被虚拟内存重置。
参数 flags 必须为 MAP_SHARED 或者 MAP_PRIVATE 二者之一的类型。MAP_SHARED 类型表示多个进程使用的是一个内存映射的副任何一个进程都可对此映射进行修改,其他的进程对此修改是可见的。而 MAP_PRIVATE 则是多个进程使用的文件内存映射,在写入操作后,会复制一个副本给修改的进程,多个进程之间的副本是不一致的。
2. munmap() 函数介绍
与 mmap() 函数对应的函数是 munmap() 函数,它的作用是取消 mmap() 函数的映射关系。 其函数原型如下:
#include <sys/mman.h>
int munmap(void *start, size_t length);
参数 start 为 mmap() 函数成功后的返回值,即映射的内存地址;参数 length 为映射的长度。
使用 mmap() 函数需要遵循一定的编程模式,其模式如下:首先使用 open() 函数打开一个文件,当操作成功的时候会返回一个文件描述符;使用 mmap() 函数将此文件描述符所代表的文件映射到一个地址空间,如果映射成功,会返回一个映射的地址指针;对文件的操作可以通过 mmap() 数映射的地址来进行,包括读数据、写数据、偏移等,与一般的指针操作相同,不过要注意不要进行越界操作;当对文件的操作完毕后,需要使用 munmap() 函数将 mmap() 函数映射的地址取消并关闭打开的文件。
/* 打开文件 */
fd = open(filename, flags, mode);
if( fd < 0 )
......(错误处理)
ptr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
/* 对文件进行操作 */
......
/* 取消映射关系 */
munmap(ptr, len);
/* 关闭文件 */
close(fd);
3. mmap() 函数和 munmap() 函数的例子
下面的代码是一个使用 mmap() 函数映射文件的实例。先打开文件 mmap.txt,并使用 mmap() 函数进行地址交间映射,当映射成功后会对文件映射地址区域进行 memset() 函数操作,然后返回。程序运行后会发现对内存地址的操作都显示在文件中。
/* 文件 mmap_01.c,使用 mmap 对文件进行操作 */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h> /* mmap */
#include <string.h> /* memset warning */
#include <stdio.h>
#define FILELENGTH 80
int main(void)
{
int fd = -1;
char buf[] ="quick brown fox jumps over the lazy dog"; /* 将要写入文件的字符库 */
char *ptr = NULL;
/* 打开文件 mmap.txt ,并将文件长度缩小为 0, 如果文件不存在则创建它,权限为可读写执行 */
fd = open("mmap.txt", O_RDWR /* 可读写 */|O__CREAT/* 不存在,创建 */|O_TRUNC/*缩小为 0*/, S_IRWXU);
if( -1 == fd) /* 打开文件失败,退出 */
{
return -1;
}
/* 下面的代码将文件的长度扩大为 80 */
/* 向后偏移文件的偏移量到 79 */
lseek(fd, FILELENGTH-1, SEEK_SET);
write(fd, "a", 1); /* 随意写入一个字符,此时文件的长度为 80 */
/* 将文件 mmap.txt 中的数据段从开头到 1M 的数据映射到内存中,对文件的操作立刻显示在文件上,可读写 */
ptr = (char*)mmap(NULL, FILELENGTH, PROT_READ | PROT_WRITE,MAP_SHARED, fd, 0);
if ( (char*)-1 == ptr) /* 如果映射失敗,则退出 */
{
printf("mmap failure\n");
close(fd);
return -1;
}
memcpy(ptr+16, buf, strlen(buf));
/* 将 buf 中的字符串复制到映射区域中,起始地址为 ptr 偏移 16 */
munmap(ptr, FILELENGTH); /* 取消文件映射关系 */
close(fd); /* 关闭文件 */
return 0;
}
上述代码首先利用函数 lseek() 偏移到 80,将文件的大小扩展为 80,并写入一个字符 a,将文件形成一个空洞文件。然后将文件从开始到结尾的 80 个字节映射到内存空间,并将地址传给 ptr,把 buf 中的字符串 quick brown fox jumps over the lazy dog 复制到 ptr 后面 16 字节开始的空间后取消映射,并关闭文件。
将代码存入文件 mmap-01.c 中,编译并运行文件:
$gcc -o mmap mmap_01.c
$./mmap
会发现当前目录下多了一个名为 mmap.txt 的文件,用 ls 査看,大小为 80 字节。
$ls -l mmap.txt
-rwx--- 1 linux-c linux-c 80 5月 31 15:52 mmap.txt
使用十六进制査看 mmap.txt 对应的 ASCII 码:
$ od -c mmap.txt
0000000 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
0000020 q u i c k b r o w n f o x
0000040 j u m p s o v e r t h e l
0000060 a z y d o g \0 \0 \0 \0 \0 \0 \0 \0 \0
0000100 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 a
0000120
十、文件属性 fcntl() 函数
fcntl() 函数用于获得和改变已经打开文件的性质。
1. fcntl() 函数介绍
fcntl() 函数向打开的文件 fd 发送命令,更改其属性。函数原型如下:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
如果操作成功,其返回值依赖于 cmd,如果出错返回值为 -1。下面的 4 个命令有特殊的返回值:F_DUPFD,F_GETFD,F_GETFL,以及 F_GETOWN。第 1 个命令(F_DUPFD)返回值为新的文件描述符,第 2 个命令(F_GETFD)返回值为获得的相应标识,第 3 个命令(F_GETFL)返回值为文件描述符的状态标志,第 4 个命令(F_GETOWN)返回值如果为正数则是进程 ID 号,如果为负数则是进程组 ID 号。
在大部分情况下,第 3 个参数总是一个整数,但是某些情况下使用记录锁时,第 3 个参数则是一个指向结构的指针。
函数 fcntl() 的功能分为以下 6 类:
█ 复制文件描述符(cmd=F_DUPFD);
█ 获得/设置文件描述符(cmd=F_GETFD 或者 F_SETFD);
█ 获得/设置文件状态值(cmd=F_GETFL 或者 F_SETFL);
█ 获得/设置信号发送对象(cmd=F_GETOWN、F_SETOWN、F_GETSIG 或者 F_SETSIG );
█ 获得/设置记录锁(cmd=F_GETLK、F_SETLK 或者 F_SETLKW);
█ 获得/设置文件租约(cmd=F_GETLEASE 或者 F_SETLEASE)。
接下来对常用的前面 4 类进行简单地介绍,记录锁和文件租约的获取和设置不常用小伙伴可以自己去阅相关资料。
█ F_DUPFD:命令用于复制文件描述符 fd,获得的新文件描述符作为函数值返回。获得的文件描述符是尚未使用的文件描述符中大于或等于第 3 个参数值中的最小值。
█ F_GETFD:获得文件描述符。
█ F_SETFD:设置文件描述符。
█ F_GETFL:标志获得文件描述符 fd 的文件状态标志,标志的含义在下表(fcntl 的文件状态标志值及含义)中列出。
文件状态值 | 含义 | 文件状态值 | 含义 |
---|---|---|---|
O_RDONLY | 只读 | O_NONBLOCK | 非阻塞方式 |
O_WRONLY | 只写 | O_SYNC | 写等待 |
O_RDWR | 读写 | O_ASYNC | 同步方式 |
O_APPEND | 写入是添加至文件末尾 |
由于 3 种存取方式(O_RDONLY、O_WRONLY 和 O_RDWR)并不是各占 1 位,这 3 个值分别为 0、1、2,要正确地获得它们的值,只能用 O_ACCMODE 获得存取位,然后与这 3 种方式比较。
2. F_GETFL 的例子
下面的代码为使用 F_GETFL 的实例,获得标准输入的存取方式,并打印出来:
/* 文件 fcntl_01.c ,使用 fcntl 控制文件符 */
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
int flags = -1;
int accmode = -1;
flags = fcntl(0, F_GETFL, 0); /* 获得标准输入的状态的状态 */
if( flags < 0 ) /* 错误发生 */
{
printf("failure to use fcntl\n");
return -1;
}
accmode = flags & O_ACCMODE; /* 获得访问模式 */
if(accmode == O_RDONLY) /* 只读 */
{
printf("STDIN READ ONLY\n");
}
else if(accmode == O__WRONLY) /* 只写 */
{
printf("STDIN WRITE ONLY\n");
}
else if(accmode == O_RDWR) /* 可读写 */
{
printf("STDIN READ WRITE\n");
}
else /* 其他棋式 */
{
printf("STDIN UNKNOWN MODE");
}
if( flags & O_APPEND ) /* 附加模式 */
{
printf("STDIN APPEND\n");
}
if( flags & O_NONBLOCK) /* 非阻塞模式 */
{
printf("STDIN NONBLOCK\n");
}
return 0;
}
将代码存入文件 fcntl_01.c 中,编译并运行文件,获得了标准的输入状态,说明标准输入是可读写的:
$gcc -o fcntl_01 fcntl_01.c
$ ./fcntl_01
STDIN READ WRITE
3. F_SETFL 的例子
F_SETFL 设置文件状态标志的值,此时用到了第 3 个参数。其中 O_RDONLY、O_WRONLY、O_RDWR、O_CREAT、O_EXCL、O_NOCTTY 和 O_TRUNC 不受影响, 可以更改的几个标志是 O_APPEND、O_ASYNC、O_SYNC、O_DIRECT、O_NOATIME 和 O_NONBLOCK。
如下代码为修改文件状态值的一个实例,在文本文件 test.txt 中的内容是 “1234567890abcdefg” 打开文件 test.txt 时设置为 O_RDWR,此时文件的偏移量位于文件开头,修改状态值的时候增加 O_APPEND 项,此时文件的偏移虽移到文件末尾,写入字符串 FCNTL,然后关闭文件。
/* 文件 fcntl_02.c ,使用 fcntl 修改文件的状态值 */
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h> /* strlen() 函数 */
int main(void)
{
int flags = -1;
char buf[] = "FCNTL";
int fd = open("test.txt”, O_RDWR);
flags = fcntl(fd, F_GETFL, O); /* 获得文件状态 */
flags |= O_APPEND; /* 增加状态为可追加 */
flags = fcntl(fd, F_SETFL, &flags); /* 将状态写入 */
if( flags < 0 ) /* 错误发生 */
{
printf("failure to use fcntl\n");
return -1;
}
write(fd, buf, strlen(buf)); /* 向文件中写入字符串 */
close(fd);
return 0;
}
将代码存入文件 fcntl-02.c 中,编译此文件:
$gcc -o fctnl-02 fcntl-02.c
没有运行 fctnl_02 之前,test.txt 文件中的内容如下:
$ od -c test.txt
0000000 1 2 3 4 5 6 7 8 9 0 a b c d e f
0000020 g \n
0000021
运行 fctnl_02,并检查文件 test.txt 中的内容:
$./fcntl_02
$ od -c test.txt
0000000 1 2 3 4 5 6 7 8 9 0 a b c d e f
0000020 g \n F C N T L
0000027
可见,修改状态后的 flags=fcntl(fd,F_SETFL,flags); 函数起了作用,文件的状态己经增加了 O_APPEND 的属性,文件的偏移量发生了变化,移到了文件 test.txt 的末尾。
4. F_GETOWN 的例子
F_GETOWN 获得接收信号 SIGIO 和 SIGURG 信号的进程 ID 或进程组 ID。例如,如下代码得到接收信号的进程 ID 号。
/* 文件 fcntl-04.c,使用 fcntl 获得接收信号的进程 ID */
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
int uid;
int fd = open("test. txt", O_RDWR); /* 打开文件 test. txt */
uid = fcntl(fd, F_GETOWN) ; /* 获得接收信号的进程 ID*/
printf("the SIG recv ID is %d\n",uid);
close(fd);
return 0;
}
5. F_SETOWN 的例子
F_SETOWN 用于设置接收信号 SIGIO 和 SIGURG 信号的进程 ID 或进程组 ID。参数 arg 为正时设置接收信号的进程 ID,arg 的值为负值时设罝接收信号的进程组 ID 为 arg 绝对值。下面的代码将文件 test.txt 的信号接收设置给进程 10000。
/* 文件 fcntl_05.c ,使用 fcntl 设置接收信号的进程 ID */
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(void)
{
int uid;
int fd = open("test.txt", O_RDWR); /* 打开文件 test.txt */
uid = fcntl(fd, F_SETOWN,10000); /* 设置接收信号的进程 ID */
close(fd);
return 0;
}
十一、文件输入输出控制 ioctl() 函数
ioctl() 是 input output control 的简写,表示输入输出控制,ioctl() 函数通过对文件描述符的发送命令来控制设备。
ioctl 用于向设备发控制和配置命令,有些命令也需要读写一些数据,但这些数据是不能用 read/write 读写的,称为 Out-of-band 数据。也就是说,read/write 读写的数据是 in-band 数据,是 I/O 操作的主体,而 ioctl 命令传送的是控制信息,其中的数据是辅助的数据。例如,在串口线上收发数据通过 read/write 操作,而串口的波特率、校验位、停止位通过 ioctl 设置,A/D 转换的结果通过 read 读取,而 A/D 转换的精度和工作频率通过 ioctl 设置。
1. ioctl() 函数介绍
ioctl() 函数的原型如下,ioctl() 函数通过对文件描述符发送特定的命令来控制文件描述符所代表的设备。参数 d 是个已经打开的设备文件描述符。通常情况下函数出错返回 -1,成功则返回 0 或者大于 1 的值,取决于对应设备的驱动程序对命令的处理。request 是 ioctl 的命令,可变参数取决于 request,通常是一个指向变量或结构体的指针。若出错则返回 -1,若成功则返回其他值,返回值也是取决于
request。
#include <sys/ioctl.h>
int ioctl(int d, int request,...);
使用 ioctI() 像其他的系统调用一样:打开文件,发送命令,查询结果。ioctl() 函数像一个杂货铺,对设备的控制通常都通过这个函数来实行。具体对设备的操作方式取决于设备驱动程序的编写。
2. ioctl() 函数的例子
下面是一个控制 CDROM 打开的简单程序,因为 CDROM 控制程序的数据结构在头文件 <linux/cdrom.h> 中,所以要包含此文件。此处使用 ioctl() 函数,仅仅发送特定的打开命令到 Linux 内核程序,用 CDROMEJECT 函数本身就可以进行区分,所以没有传入配置数据。
/* 文件 ioctl_01. c 控制 CDROM */
#include <linux/cdrom.h>
#include <stdio.h>
#include <fcntl.h>
int main(void)
{
int fd = open("/dev/cdrom",O_RDONLY|O_NONBLOCK); /* 打开 CDROM 设备文件 */
if(fd < 0)
{
printf("打开 CDROM 失败\n");
return -1;
}
/* 向 Linux 内核的 CDROM 驱动程序发送 CDROMEJECT 请求 */
if(!ioctl(fd,CDROMEJECT,NULL); /* 驱动程序操作成功 */
{
printf("成功弹出 CDROM \n");
}
else /* 驱动程序操作失败 */
{
printf("弹出 CDROM 失败\n");
}
close(fd); /* 关闭 CDROM 设备文件 */
return 0;
}
socket 文件类型
在 Linux 下面还有一类比较特殊的文件,即 socket 文件。它是一种网络接口的抽象,与普通文件一样,socket 文件描述符支持 read() 函数、write() 函数操作,并可以使用 fcntl() 函数进行文件控制。
小结
Linux 的文件系统是一个树状的结构,通过虚拟文件系统 VFS ,Linux 在各种各样的文件系统上面建立了统一的操作 API,例如读数据、写数据等。这种抽象机制不仅仅对普通文件有效,同样可以操作各种各样的设备,例如帧缓冲设备等。对文件进行编程的函数比较琐碎,这是由于使用的多样性造成的。
文件的 lseek 操作是进行文件偏移量指针的移动,可以将其移到超出文件末尾的位置,来制造 “空洞” 文件。
mmap() 函数是一种将文件和地址空间进行映射的方法,映射成功后,可以像操作内存一样对文件内容进行读写。
ioctl() 函数和 fcntl() 函数都是控制的接口,控制的内容由传送的命令决定,这是内核和应用层直接通信的一种方法。
在 Linux 上目录也是文件的一种,操作方式与文件一致。