文件操作的系统调用接口:
文件是Linux系统中的重要概念。它不仅仅是对普通文件的操作接口,也是设备通信、进程间通信、网络通信的重要编程接口。因
此文件操作的相关调用也是Linux内核提供的最重要的编程接口。
本节将重点叙述如下几个常用的文件操作系统调用。
open:打开文件。
read:从已打开的文件中读取数据。
write:向已打开的文件中写入数据。
close:关闭已打开的文件。
ioctl:向文件传递控制信息或发出控制命令。
对文件的操作工程一般是这样的:先打开文件,内核对打开的文件进行管理,打开成功后应用程序将获得文件描述符;然后应用程
序使用文件描述符对文件进行读写操作;当全部操作完毕后,应用程序需要将文件关闭以释放用于管理打开文件的内存。
文件描述符是一个取值从0开始的整数。内核默认一个进程同时打开的文件数有一个上限,也就是文件描述符取值的上限,一般是
1024。
每个进程在启动后就默认有三个打开的文件描述符0,1,2,如果启动程序时没有进行重定向,则文件描述符0关联到标准输入,1关
联到标准输出,2关联到标准错误输出。在C库函数中可以使用以下几个宏来表示这几个文件描述符:
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
打开文件:
在访问文件之前,首先应打开文件。可以使用open或creat函数来打开文件,它们的接口头文件及函数原型如下:
#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);
int creat(const char *pathname, mode_t mode);
其各个参数及返回值的含义解释如下.
◆ pathname:要打开的文件名称。
◆ flags:标志位,指定打开文件的操作方式及打开时的一些行为。
◆ mode: 用于指定新文件的权限标志位。
◆ 返回值:操作成功则返回文件描述符,否则返回-1并设置标量errno的值。
flags参数有以下几个基本的取值。
◆ O_RDONLY:以只读方式打开文件。
◆ O_WRONLY:以只写方式打开文件。
◆ O_RDWR:以读写方式打开文件。
这几个标志位指定打开文件的操作方式,它们是互斥的,不能同时使用,但可以与下述标志用按位或的方式组合起来使用。
◆ O_CREAT:如果被打开的文件不存在,则自动创建这个文件。
◆ O_EXCL:如果O_CREAT标志已经使用,那么当由pathname参数指定的文件已经存在时open函数返回失败。如果pathname给出的是
一个符号链接,无论它指向的文件是否存在,对open函数的调用都会返回失败。
◆ O_NOCITY:如果被打开的文件是一个终端设备文件(如/dev/tty),它不会成为这个进程的控制终端。
◆ O_TRUNC:如果被打开的文件存在并且是以可写的方式打开的,则清空文件原有内容。
◆ O_APPEND:新写入的内容将被附加在文件原来的内容之后,即打开后文件的读写位置被置于文件尾。
◆ O_NONBLOCK:被打开的文件将以非阻塞的方式进行操作。
◆ O_NDELAY:同O_NONBLOCK。
◆ O_SYNC:被打开的文件将以同步I/O的方式进行操作,即任何写操作都会先被同步到硬件设备上。同步完成后,对写函数的调用
才返回。
◆ O_NOFOLLOW:如果pathname是一个符号链接,则对open函数的调用将返回失败。
◆ O_DIRECTORY:如果pathname不是目录,则对open函数的调用将返回失败。
需要注意的是,open函数有两个原型,其中一个多出了个参数mode,它用于指定创建的新文件的访问权限。如果打开时使用了
O_CREAT标志创建新文件,则一般都要给出mode参数,它的一些常用取值如表所示,这些值可以用按位或的方式组合使用。新文件的所属用
户和所属组则是创建它的进程的所属用户和所属组。
权限标志定义 对应的八进制形式 含义
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 其他用户有执行权限
鉴于在调用open函数时,O_WRONLY,O_CREAT,O_TRUNC三个标志位经常组合使用,因此由一个专门的函数creat来实现。如下:
creat(pathname, mode);
实际上等价于:
open(pathname,O_WRONLY|O_CREAT|O_TRUNC,mode);
这两个函数在打开文件成功时将返回一个文件描述符,可用于随后的read/write或其他对文件的操作使用。两个不同的进程打开同
一个文件是允许的,但它们得到的文件描述符一般是不同的。如果它们都对文件进行写操作,就会出现数据不一致的情况,也就是最后写入
的可能覆盖先前其他进程写入的内容,这就涉及到进程间数据共享和同步的概念了。
如果打开操作失败,这两个函数会返回-1,并将errno变量设置为一个合适的错误值。
从文件读取数据:
文件打开后就可以进行读写操作了。读操作的接口头文件及函数原型如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
其各个参数及返回值的含义解释如下:
◆ fd:要读取的文件描述符。
◆ buf:指向读取到的数据要放入的缓冲区。(buf指向的内存空间必须事先分配好)
◆ count:要读取的字节数(缓冲区大小)。
◆ 返回值:读取到的字节数,失败返回-1,并设置标量errno的值。
这里的size_t型实际上就是无符号整型,而ssize_t型就是有符号整型。
这个函数将从fd代表的文件的当前读写位置读取不超过count个字节到buf指向的内存中,并返回读到的字节数。
对于普通文件来说,读操作完成后,文件的读写位置会向后移动,移动的长度就是读取的字节数,下一次读操作将从新的读写位置
开始。
read函数的返回值小于指定的count是可能的,并不是错误。出现这种情况有各种原因,比如,文件本身可供读取的字节数比count
小或者read系统调用被信号打断等。read系统调用看似简单,但实际上对于各种可能情况的处理是比较复杂的,因为I/O操作有很多异常情
况要考虑到。下面列出了read系统调用中可能遇到的情况及其处理方法。
◆ 调用返回值等于count,读取的数据存放在buf指向的内存中,结果与预期一致。
◆ 调用返回一个大于0小于count的值,读取的字节数存放在buf指向的内存中。出现这种情况可能是一个信号打断了读取过程,或
在读取中发生了一个错误,或读取的有效字节数大于0但不足count个,或在读入count个字节前文件已经结束。如果读取
的过程被信号打断则可以再次进行读取。
◆ 调用返回0,说明文件已结束,没有可以读入的数据。
◆ 调用阻塞,说明没有可读的数据,这种情况下如果以非阻塞方式操作文件,那么会立即返回错误。
◆ 调用返回-1,并且errno变量被设置为EINTR,表示在读入有效字节前收到一个信号,这种情况可以重新进行读操作。
◆ 调用返回-1,并且errno变量被设置为EAGAIN,这说明是在非阻塞方式下读文件,并且没有可读的数据。
◆ 调用返回-1,并且errno变量被设置为非EINTR或EAGAIN的值,表示有其他类型的错误发生,必须根据具体情况进行处理。
由于有各种异常情况的存在,为了从文件中可靠的读取指定的字节数,就必须对这些情况进行处理,必要时需重新进行读操作。这
对于设备文件、管道或socket来说尤其有意义。例如,用下面的代码能够可靠的从文件中读取指定的字节数(假设文件是以阻塞的方式进行
操作的):
ssize_t ret;
while(len != 0 && (ret = read(fd,buf,len)) != 0)
{
if(ret == -1)
{
if(errno == EINTR) continue;
perror("read");
break;
}
len -= ret;
buf += ret;
}
这里把操作放在循环中,当一次读操作没有得到len个字节时,将调整len和buf的值继续进行操作。如果读操作返回-1,说明有错
误发生,这时如果错误码是EINTR,说明只是被信号打断,可以继续读,其他情况则被认为是严重的错误,不能再继续读。
以上是以阻塞方式读文件的例子,这时,如果文件是一个设备文件并且设备没有可读的数据,进程将进入睡眠状态不再继续执行,
或者说阻塞在read系统调用处,直到设备有了可读的数据才会被唤醒继续执行。很多时候我们需要进程能够立刻返回以处理其他的事物,那
么就需要采用非阻塞的方式来操作文件,举例如下:
ssize_t nr;
start:
nr = read(fd, buf, len);
if(nr == -1)
{
if(errno == EINTR) goto start;
if(errno == EAGAIN)
{
/*处理其他事物,在恰当时再调用read*/
}
else
{
/*有错误发生,处理错误*/
}
}
可以看到,在采用非阻塞的方式读文件时,如果读操作返回-1,我们必须检查错误码是否为EINTR或EAGAIN,如果是EINTR,可以再
次进行读操作,而如果是EAGAIN,表示要读取的(设备)文件现在没有可供读取的数据,因此进程可以继续处理其他事物,而后在恰当的时
机再来读取这个文件。
写数据到文件:
向打开文件中写入数据的接口头文件及函数原型如下:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
其各个参数及返回值的含义解释如下。
◆ fd:要写入的文件的描述符。
◆ buf:指向要写入的数据所存放的缓冲区。
◆ count:要写入的字节数。
◆ 返回值:实际写入的字节数,失败则返回-1,并设置变量errno的值。
这个函数会从fd所代表的文件当前读写位置开始,把buf指向的内存中最多count个字节写入文件。写入成功则返回写入的字节数,
并更新文件的读写位置。
write系统调用返回大于0而小于count的值是合法的,并不表示有错误发生。
◆ 调用返回值等于count,说明数据全部写入成功。
◆ 调用返回一个大于0小于count的值,说明部分数据没有写入。这可能是因为写入过程被信号打断,或者底层的设备暂时没有足
够的空间存放写入的数据。
◆ 调用阻塞,说明暂时不能写入数据,这种情况下如果以非阻塞方式操作文件,那么会立即返回错误。
◆ 调用返回-1,并且errno变量被设置为EINTR,表示在写入一个有效字节前,收到一个信号,应用程序可以再次进行写操作。
◆ 调用返回-1,并且errno变量被设置为EAGAIN,说明是在非阻塞方式下写文件但文件暂时不能写入数据。
◆ 调用返回-1,并且errno变量被设置为EBADF,表示给定的文件描述符非法,或者文件不是以写方式打开。
◆ 调用返回-1,并且errno变量被设置为EFAULT,表示buf是无效的指针。
◆ 调用返回-1,并且errno变量被设置为EFBIG,表示写入的数据超过了最大的文件尺寸,或者超过了允许的文件读写位置。
◆ 调用返回-1,并且errno变量被设置为EPIPE,说明写入时发生了数据通道断层的错误,这种情况只在文件是管道或者是socket
的情况下发生。在这种情况下,进程还将收到一个SIGPIPE信号,信号的默认处理程序是使进程退出。
◆ 调用返回-1,并且errno变量被设置为ENOSPC,说明底层设备没有足够的空间。
写文件举例如下:
ssize_t ret;
while(len != 0 && (ret = write(fd, buf, len)) != 0)
{
if(ret == -1)
{
if(errno == EINTR) continue;
perror("write");
break;
}
len -= ret;
buf += ret;
}
这里假定写操作是以阻塞的方式进行的。如果以非阻塞方式进行写操作,则当函数返回-1时,必须检测errno变量的值是否为
EAGAIN,以决定能否再进行写操作。
发送控制命令:
在Linux系统上,那些不能被抽象为读和写的文件操作统一由ioctl操作代表。ioctl操作用于向文件发送控制命令,这些命令不能
被视为是输入输出流的一部分,而只是影响文件的操作方式。对于设备文件来说,ioctl操作常用于修改设备的参数。
ioctl系统调用的接口头文件及函数原型如下:
#include <sys/ioctl.h>
int ioctl(int fd, int request, ...);
◆ fd:要操作的文件描述符。
◆ request:代表要进行的操作,不同的(设备)文件有不同的定义。
◆ 可变参数:取决于request参数,通常是一个指向变量或结构体的指针。
◆ 返回值:成功返回0,有效ioctl操作返回其他非负值,错误返回-1。
ioctl能够进行的操作根据fd所代表的文件的具体类型而变化,非常繁多。下面举一个例子,使用TIOCGWINSZ命令获得终端的窗口
大小,如下:
/*文件名:console_size.c*/ /*说明:使用ioctl获得控制台窗口的大小*/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> int main(void) { struct winsize size; /*判断标准输出是否为tty设备,防止输出被重定向的情况*/ if(!isatty(STDOUT_FILENO) < 0) return -1; /*获得窗口大小*/ if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) < 0) { perror("ioctl TIOCGWINSZ error"); return -1; } /*输出结果*/ printf("rows is %d, columns is %d\n", size.ws_row, size.ws_col); return 0; }
关闭文件:
程序完成对文件的操作后,要使用close系统调用将文件关闭,其接口头文件与函数原型如下:
#include <unistd.h>
int close(int fd);
fd是要关闭的文件的描述符,返回值在操作成功的情况下是0,否则是-1.