一、Linux中文件及文件描述符概述
在Linux中对目录和设备的操作都等同于文件的操作,Linux中文件主要分为4种:普通文件、目录文件、链接文件和设备文件。
内核通过文件描述符来区分和引用特定的文件。对Linux而言,所有设备和文件的操作都是使用文件描述符来进行的。文件描述符是一个非负的整数,它是一个索引值,并指向在内核中每个进程打开文件的记录表。当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;当需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。
通常,一个进程启动时都会打开三个文件:标准输入、标准输出和出错处理。这三个文件分别对应文件描述法为0、1和2(也就是替换宏STDIN_FILENO、STDOUT_FILEON和STDERR_FILENO)。
二、底层文件I/O操作
1.基本文件操作
(1)open()函数用于打开或创建文件,在打开或创建文件时可以指定用户的属性及用户的权限等各种参数。
open()函数语法要点
所需头文件 |
#include
/* 提供类型pid_t的定义 */
#include
#include
函数原型 |
int open(const char *pathname,int flags,int perms)
函数传入值 |
pathname
被打开的文件名(包括路径名)
flag:文件打开的方式
O_RDONLY:以只读方式打开文件
O_WRONLY:以只写方式打开文件
O_RDWR:以读写方式打开文件
O_CREAT:如果该文件不存在,就创建一个新的文件,并用第三个参数为其设置权限
O_EXCL:如果使用O_CREAT时文件存在,则可返回错误消息。这一参数可测试文件是否存在。此时open是原子操作,防止多个进程同时创建同一个文件
O_NOCTTY:使用本参数时,若文件为终端,那么该终端不会成为调用open()的那个进程的控制终端
O_TRUNC:若文件已经存在,那么会删除文件中的所有数据,并设置文件大小为0
O_APPEND:以添加方式打开文件,在打开文件的同时,文件指针指向文件的末尾,即将写入的数据添加到文件的末尾
perms
被打开文件的存取权限
可以用一组宏定义:S_I(R/W/X)(USR/GRP/OTH)
其中R/W/X分别表示读/写/执行权限
USR/GRP/OTH分别表示文件所有者/文件所属组/其他用户
例如,S_IRUSR|S_IWUSR表示设置文件所有者的可读可写属性。
八进制表示法中600也表示同样的权限
函数返回值 |
成功:返回文件描述符
失败:-1
PS:在open()函数中,flag参数可通过“|”组合构成,但前3个标志常量(O_RDONLY、O_WRONLY以及O_RDWR)不能相互组合。perms是文件的存取权限,既可以用宏定义表示法,也可以用八进制表示法。
(2)close()函数用于关闭一个被打开的文件。当一个进程终止时,所有被它打开的文件都由内核自动关闭,很多程序都使用这一功能而不显示地关闭一个文件。
close()函数语法要点
所需头文件 |
#include
函数原型 |
int close(int fd)
函数输入值 |
fd:文件描述符
函数返回值 |
0:成功
-1:出错
(3)read()函数用于将从指定的文件描述符中读出数据放到缓存区中,并返回实际读入的字节数。若返回0,则表示没有数据可读,即已达到文件尾。读操作从文件的当前制作位置开始。当从终端设备文件中读出数据时,通常最多一次读一行。
read()函数语法要点
所需头文件 |
#include
函数原型 |
ssize_t read(int fd,void *buf,size_t count)
函数传入值 |
fd:文件描述符
buf:指定存储器读出数据的缓冲区
count:指定读出的字节数
函数返回值 |
成功:读到的字节数
0:已到达文件尾
-1:出错
(4)write()函数用于向打开的文件写数据,写操作从文件的当前指针位置开始。对磁盘文件进行写操作,若磁盘已满或超出该文件的长度,则write()函数返回失败。
write()函数语法要点
所需头文件 |
#include
函数原型 |
ssize_t write(int fd,void *buf,size_t count)
函数传入值 |
fd:文件描述符
buf:指定存储器写入数据的缓冲区
count:指定写入的字节数
函数返回值 |
成功:已写入的字节数
-1:出错
(5)lseek()函数用于在指定的文件描述符中将文件指针指定到相应的位置。它只能用在可定位(可随机访问)文件操作中。管道、套接字和大部分字符设备室不可定位的,所以在这些文件的操作中无法使用lseek()调用。
lseek()函数语法要点
所需头文件 |
#include
#include
函数原型 |
off_t lseek(int fd,off_t offset,int whence)
函数传入值 |
fd:文件描述符
offset:偏移量,每一读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)
|
whence:当前位置的基点
SEEK_SET:当前位置为头文件的开头,新位置为偏移量的大小
SEEK_CUR:当前位置为文件指针的位置,新位置为当前位置加上偏移量
SEEK_END:当前位置为文件的结尾,新位置为文件的大小加上偏移量的大小
函数返回值 |
成功:文件的当前位移
-1:出错
2.文件锁
当文件可以同时被多个进程使用时,为了避免共享资源产生竞态,Linux通常采用的方法是给文件上锁——文件锁有两种:建议性锁和强制性锁。建议性锁要求每个上锁文件的进程都要检查是否有锁存在,并且尊重已有的锁。强制锁是由内核执行的锁,当一个文件被上锁进行写入操作的时候,内核将阻止任何其他文件对其进行读写操作。
在Linux中,实现上锁的函数有lockf()和fcntl(),lockf()用于对文件施加建议性锁,而fcntl()可以施加建议性锁和强制锁,同时fcntl()还能对文件的某一记录上锁——记录锁。
记录锁又分为读取锁(共享锁)和写入锁(排斥锁)。读取锁可以使多个进程都在文件的统一部分建立读取锁。而写入锁使在任何时刻只能有一个进程在文件的某一部分建立写入锁。PS:在文件的同一部分不能同时建立读取锁和写入锁。
fcntl()函数语法要点
所需头文件 |
#include
#include
#include
函数原型 |
int fcntl(int fd,int cmd,struct flock *lock)
函数传入值 |
fd:文件描述符
cmd
F_DUPFD:复制文件描述符
F_GETFD:获得fd的close-on-exec标志,若标志未设置,则文件经过exec()函数之后仍保持打开状态
F_SETFD:设置close-on-exec标志,该标志由参数arg的FD_CLOEXEC位决定
F_GETFL:得到open设置的标志
F_SETFL:改变open设置的标志
F_GETLK:根据lock参数值,决定是否上文件锁
F_SETLK:设置lock参数值的文件锁
F_SETLKW:这是F_SETLK的阻塞版本(命令名中的W表示等待(wait))。在无法获取锁时,会进入睡眠状态;如果可以获取锁或者捕捉到信号则会返回
lock:结构为flock,设置记录锁的具体状态
函数返回值 |
0:成功
-1:出错
lock的结构如下所示:
struct flock
{
short l_type;
//F_RDLCK:读取锁(共享锁)
//F_WRLCK:写入锁(排斥锁)
//F_UNLCK:解锁
off_t l_start; //相对位移量(字节)
short l_whence; //相对位移量的起点
//SEEK_SET:当前位置为文件的开头,新位置为偏移量的大小
//SEEK_CUR:当前位置为文件指针的位置,新位置为当前位置加上偏移量
//SEEK_END:当前位置为文件的结尾,新位置为文件的大小加上偏移量的大小
off_t l_len; //加锁区域的长度
pid_t l_pid;
}
PS:如果想为整个文件加锁,可以将l_start设置为0,l_whence设置为SEEK_SET,l_len设置为0。
3.多路复用(select()和poll())
(1)I/O处理模型有5中:
①阻塞I/O模型:在这种模型下,若所调用的I/O函数没有完成相关的功能,则会使进程挂起,直到相关数据到达才会返回。对管道设备、终端设备和网络设备进行读写时经常出现这种情况。
②非阻塞模型:在这种模型下,当请求的IO操作不能完成任务时,则不让进程休眠,而且立即返回。
③IO多路转接模型:在这种模型下,如果请求的IO操作阻塞,且它不是真正的阻塞IO,而是让其中的一个函数等待,在这其间IO还能进行其他操作。select()和poll()就属于这种类型。
④信号驱动IO模型:在这种模型下,通过安装一个信号处理程序,系统可以自动捕获特定的信号的到来,从而启动IO。这是由内核通知用户何时可以启动一个IO操作决定的。
⑤异步IO模型:在这种模型下,当一个描述符已准备好,可以启动IO时,进程会通知内核。
(2)select():该函数可以设置程序中每个关系的文件描述符的条件、希望等待的时间等。返回时内核会通知用户已经准备好的文件描述符的数量、已经准备好的条件等。
select()函数语法要点
所需头文件 |
#include
#include
#include
函数原型 |
int select(int numfds,fd_set *readfds,fd_set *writefds,fd_set *exeptfds,struct timeval *timeout)
函数传入值 |
numfds:该参数值为需要监视的文件描述法的最大值加1
readfds:由select()监视的读文件描述符集合
writefds:由select()监视的写文件描述符集合
exeptfds:由select()监视的异常处理文件描述符集合
timeout
NULL:永远等待,直到捕捉到信号或文件描述符已准备好为止
具体值:struct timeval 类型的指针,若等待了timeout时间还没有检测到任何文件描述符准备好,就立即返回
0:从不等待,测试所有指定的描述符并立即返回
函数返回值 |
大于0:成功,返回准备好的文件描述符的数量
0:超时
-1:出错
对文件描述符进行处理的宏函数:
select()文件描述符处理函数
FD_ZERO(fd_set *set)
清除一个文件描述符集合
FD_SET(int fd,fd_set *set)
将一个文件描述符加入文件描述符集合
FD_CLR(int fd,fd_set *set)
将一个文件描述法从文件描述符集中清除
FD_ISSET(int fd,fd_set *set)
如果文件描述法fd为fd_set集合中的一个元素,则返回非零值,可以用于调用select()之后测试文件描述符集合中的文件描述符是否有变化
PS:在使用select()之前,首先使用FD_ZERO()和FD_SET()来初始化文件描述符集合;
在使用select()时,循环使用FD_ISSET()来测试描述符集合;
在执行完相关描述符的操作后,使用FD_CLR()来清除描述符集合;
select()函数中的timeout是一个struct timeval类型的指针,结构体如下:
struct timeval
{
long tv_sec; /* 秒 */
long tv_unsec; /* 微妙 */
}
(3)poll()函数
poll()函数语法要点
所需头文件 |
#include
#include
函数原型 |
int poll(struct pollfd *fds,int numfds,int timeout)
函数传入值 |
fds:struct pollfd 结构的指针,用于描述需要对哪些文件的哪种类型的操作进行监控。
struct pollfd
{
int fd; /* 需要监听的文件描述符 */
short events; /* 需要监听的事件 */
short revents; /* 已发生的事情 */
}
events 成员描述需要监听哪些类型的事件,可以用以下几种标志来描述。
POLLIN:文件中有数据可读
POLLPRI:文件中有紧急数据可读
POLLOUT:可以向文件写入数据
POLLERR:文件中出现错误,只限于输出
POLLHUP:与文件的连接被断开了,只限于输出
POLLNVAL:文件描述符是不合法的,即它并没有指向一个成功打开的文件
numfds:需要监听的文件个数,即第一个参数所指向的数组中的元素数目
timeout:表示poll阻塞的超时时间(毫秒)。如果该值小于等于0,则表示无限等待
函数返回值 |
成功:返回大于0的值,表示事件发生的pollfd结构的个数
0:超时
-1:出错