Linux文件IO

1 基本文件操作

1.1函数说明

open()函数:用于打开或创建文件, 在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。


close()函数:用于关闭一个被打开的文件。 当一个进程终止时, 所有被它打开的文件都由内核自动关闭, 很多程序都使用这一功能而不显式地关闭一个文件。


read()函数:用于将从指定的文件描述符中读出的数据放到缓存区中, 并返回实际读入的字节数。 若返回 0, 则表示没有数据可读, 即已达到文件尾。 读操作从文件的当前指针位置开始。 当从终端设备文件中读出数据时, 通常一次最多读一行。


write()函数:用于向打开的文件写数据, 写操作从文件的当前指针位置开始, 对磁盘文件进行写操作, 若磁盘已满或超出该文件的长度, 则 write()函数返回失败。


lseek()函数:用于在指定的文件描述符中将文件指针定位到相应的位置。 它只能用在可定位(可随机访问) 文件操作中。 管道、 套接字和大部分字符设备文件不可定位的, 所以在这些文件的操作中无法使用 lseek()调用。


2.2 函数格式

open()函数的语法要点。

flag 参数可通过“|” 组合构成, 但前 3 个标志常量(O_RDONLY、O_WRONLY 及 O_RDWR) 不能相互组合。

perms 是文件的存取权限, 既可以用宏定义表示法, 也可以用八进制表示法,一般在加上 O_CREAT 后才使用这个参数。


close()函数的语法要点。

read()函数的语法要点。

在读普通文件时, 若读到要求的字节数前已到达文件的尾部, 则返回的字节数会小于希望读出的字节数。


write()函数的语法要点。

在写普通文件时, 写操作从文件的当前指针位置开始。


lseek()函数的语法要点。

3.3 函数使用实例

      下面实例中的 open()函数带有 3 个 flag 参数: O_CREAT、 O_TRUNC 和 O_WRONLY,这样就可以对不同的情况指定相应的处理方法。 另外, 这里对该文件的权限设置为 0600。
    下面列出文件基本操作的实例, 基本功能是从一个文件(源文件) 中读取最后 10KB 数据并复制到另一个文件(目标文件)。 在实例中源文件是以只读方式打开的, 目标文件是以只写方式打开(可以是读/写方式) 的。 若目标文件不存在, 可以创建并设置权限的初始值
为 644, 即文件所有者可读可写, 文件所属组和其他用户只能读。读者需要留意的地方是改变每次读/写的缓存大小(实例中为 1KB) 会怎样影响运行效率。

其源码如下所示:

/* copy_file.c */
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#define BUFFER_SIZE 1 /* 每次读/写缓存大小, 影响运行效率 */
#define SRC_FILE_NAME "src_file" /* 源文件名 */
#define DEST_FILE_NAME "dest_file" /* 目标文件名 */
#define OFFSET 10 /* 复制的数据大小 */
int main()
{
	int src_file, dest_file;
	unsigned char buff[BUFFER_SIZE];
	int real_read_len;
	/* 以只读方式打开源文件 */
	src_file = open(SRC_FILE_NAME, O_RDONLY);
	/* 以只写方式打开目标文件, 若此文件不存在则创建该文件, 访问权限值为 644 */
	dest_file = open(DEST_FILE_NAME, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
	if (src_file < 0 || dest_file < 0)
	{
		printf("Open file error\n");
		exit(1);
	} 
	/* 将源文件的读/写指针移到最后 10字节 的起始位置 */
	lseek(src_file, -OFFSET, SEEK_END);
	/* 读取源文件的最后 10字节 数据并写到目标文件中, 每次读写 1KB */
	while ((real_read_len = read(src_file, buff, sizeof(buff))) > 0)
	{
		write(dest_file, buff, real_read_len);
	} 
	close(dest_file);
	close(src_file);
	return 0;
}

2 文件锁

2.1 fcntl()函数说明

       前面讲述的 5 个基本函数实现了文件的打开、 读/写等基本操作, 本节将讨论在文件已经共享的情况下如何操作, 也就是当多个用户共同使用、 操作一个文件的情况。 这时, Linux通常采用的方法是给文件上锁, 来避免共享的资源产生竞争的状态。
      文件锁包括建议性锁强制性锁

建议性锁要求每个上锁文件的进程都要检查是否有锁存在, 若已有锁,可以选择继续写,也可以选择不写。 在一般情况下, 内核和系统都不使用建议性锁。

强制性锁是由内核执行的锁, 当一个文件被上锁进行写入操作时, 内核将阻止其他任何文件对其进行读写操作。 采用强制性锁对性能的影响很大, 每次读写操作都必须检查是否有锁存在。
      在 Linux 中, 实现文件上锁的函数有 lockf()和 fcntl()

lockf()用于对文件施加建议性锁 

fcntl()不仅可以施加建议性锁, 还可以施加强制性锁。 同时, fcntl()还能对文件的某一记录上锁, 也就是记录锁。
      记录锁又可分为读取锁和写入锁

读取锁又称为共享锁, 它能够使多个进程都能在文件的同一部分建立读取锁。

写入锁又称为排斥锁, 在任何时刻只能有一个进程在文件的某个部分建立写入锁。 当然, 在文件的同一部分不能同时建立读取锁和写入锁。
      fcntl()函数具有很丰富的功能, 它可以对已打开的文件描述符进行各种操作, 不仅包括管理文件锁,还包括获得设置文件描述符和文件描述符标志、文件描述符的复制等很多功能。


本文主要介绍 fcntl()函数建立记录锁的方法, 关于它的其他操作, 感兴趣的读者可以参看fcntl 手册。


2.2 fcntl()函数格式

用于建立记录锁的 fcntl()函数语法。

这里, lock 的结构如下所示:
 

struct flock {
             short l_type; /* 锁类型: F_RDLCK, F_WRLCK, F_UNLCK */
             short l_whence; /*取值为SEEK_SET(文件头), SEEK_CUR(文件当前位置), SEEK_END(文件尾) */
             off_t l_start; /* 相对于l_whence字段的偏移量 */
             off_t l_len; /* 需要锁定的长度 */
             pid_t l_pid; /* 当前获得文件锁的进程标识(F_GETLK) */
};

lock 结构中每个变量的取值含义。

为加锁整个文件, 通常的方法是将 l_start 设置为 0, l_whence 设置为 SEEK_SET, l_len设置为 0。


2.3 fcntl()使用实例

      下面首先给出了使用 fcntl()函数的文件记录锁功能的代码实现。 在该代码中, 首先给flock 结构体的对应位赋予相应的值。
      接着调用两次 fcntl()函数。 用 F_GETLK 命令判断是否可以进行 flock 结构所描述的锁操作: 若可以进行, 则 flock 结构的 l_type 会被设置为 F_UNLCK, 其他域不变; 若不可进行, 则 l_pid 被设置为拥有文件锁的进程号, 其他域不变。

      用 F_SETLK 和 F_SETLKW 命令设置 flock 结构所描述的锁操作,后者是前者的阻塞版。
       当第一次调用 fcntl()时, 使用 F_GETLK 命令获得当前文件被上锁的情况, 由此可以判断能不能进行上锁操作; 当第二次调用 fcntl()时, 使用 F_SETLKW 命令对指定文件进行上锁/解锁操作。 因为 F_SETLKW 命令是阻塞式操作, 所以, 当不能把上锁/解锁操作进行下
去时, 运行会被阻塞, 直到能够进行操作为止。

文件记录锁的功能代码具体如下所示:
 

/* lock_set.c */
int lock_set(int fd, int type)
{
	struct flock old_lock, lock;
	lock.l_whence = SEEK_SET;
	lock.l_start = 0;
	lock.l_len = 0;
	lock.l_type = type;
	lock.l_pid = -1;
	/* 判断文件是否可以上锁 */
	fcntl(fd, F_GETLK, &lock);
	if (lock.l_type != F_UNLCK)
	{
		/* 判断文件不能上锁的原因 */
		if (lock.l_type == F_RDLCK) /* 该文件已有读取锁 */
		{
			printf("Read lock already set by %d\n", lock.l_pid);
		}
		else if (lock.l_type == F_WRLCK) /* 该文件已有写入锁 */
		{
			printf("Write lock already set by %d\n", lock.l_pid);
		}
		} 
		/* l_type 可能已被 F_GETLK 修改过 */
		lock.l_type = type;
		/* 根据不同的 type 值进行阻塞式上锁或解锁 */
		if ((fcntl(fd, F_SETLKW, &lock)) < 0)
		{
			printf("Lock failed:type = %d\n", lock.l_type);
			return 1;
		} 
		switch(lock.l_type)
		{
			case F_RDLCK:
			{
				printf("Read lock set by %d\n", getpid());
			}break;
			case F_WRLCK:
			{
				printf("Write lock set by %d\n", getpid());
			}break;
			case F_UNLCK:
			{
				printf("Release lock by %d\n", getpid());
				return 1;
			}break;
			default:	break;
		}/* end of switch */
	return 0;
}

下面的实例是文件写入锁的测试用例, 这里首先创建了一个 hello 文件, 之后对其上写入锁, 最后释放写入锁。 代码如下所示:
 

/* write_lock.c */
#include <unistd.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include "lock_set.c"
int main(void)
{
	int fd;
	/* 首先打开文件 */
	fd = open("hello",O_RDWR | O_CREAT, 0644);
	if(fd < 0)
	{
		printf("Open file error\n");
		exit(1);
	} 
	/* 给文件上写入锁 */
	lock_set(fd, F_WRLCK);
	getchar();
	/* 给文件解锁 */
	lock_set(fd, F_UNLCK);
	getchar();
	close(fd);
	exit(0);
}

     为了能够使用多个终端, 更好地显示写入锁的作用, 本实例主要在 PC 上测试, 读者可将其交叉编译, 下载到目标板上运行。 下面是在 PC 上的运行结果。 为了使程序有较大的灵活性, 笔者采用文件上锁后由用户输入任意键使程序继续运行。 建议读者开启两个终端, 并且在两个终端上同时运行该程序, 以达到多个进程操作一个文件的效果。 在这里, 笔者首先运行终端一, 请读者注意终端二中的第一句。
终端一:
$ ./write_lock
write lock set by 4994
release lock by 4994
终端二:
$ ./write_lock
write lock already set by 4994
write lock set by 4997
release lock by 4997
由此可见, 写入锁为互斥锁, 同一时刻只能有一个写入锁存在。
接下来的程序是文件读取锁的测试用例, 原理与上面的程序一样。
 

/* fcntl_read.c */
#include <unistd.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include "lock_set.c"

/* lock_set.c */
int lock_set(int fd, int type)
{
	struct flock old_lock, lock;
	lock.l_whence = SEEK_SET;
	lock.l_start = 0;
	lock.l_len = 0;
	lock.l_type = type;
	lock.l_pid = -1;
	/* 判断文件是否可以上锁 */
	fcntl(fd, F_GETLK, &lock);
	if (lock.l_type != F_UNLCK)
	{
		/* 判断文件不能上锁的原因 */
		if (lock.l_type == F_RDLCK) /* 该文件已有读取锁 */
		{
			printf("Read lock already set by %d\n", lock.l_pid);
		}
		else if (lock.l_type == F_WRLCK) /* 该文件已有写入锁 */
		{
			printf("Write lock already set by %d\n", lock.l_pid);
		}
		} 
		/* l_type 可能已被 F_GETLK 修改过 */
		lock.l_type = type;
		/* 根据不同的 type 值进行阻塞式上锁或解锁 */
		if ((fcntl(fd, F_SETLKW, &lock)) < 0)
		{
			printf("Lock failed:type = %d\n", lock.l_type);
			return 1;
		} 
		switch(lock.l_type)
		{
			case F_RDLCK:
			{
				printf("Read lock set by %d\n", getpid());
			}break;
			case F_WRLCK:
			{
				printf("Write lock set by %d\n", getpid());
			}break;
			case F_UNLCK:
			{
				printf("Release lock by %d\n", getpid());
				return 1;
			}break;
			default:	break;
		}/* end of switch */
	return 0;
}

int main(void)
{
	int fd;
	fd = open("hello",O_RDWR | O_CREAT, 0644);
	if(fd < 0)
	{
		printf("Open file error\n");
		exit(1);
	} 
	/* 给文件上读取锁 */
	lock_set(fd, F_RDLCK);
	getchar();
	/* 给文件解锁 */
	lock_set(fd, F_UNLCK);
	getchar();
	close(fd);
	exit(0);
}

同样开启两个终端, 并首先启动终端一上的程序, 其运行结果如下所示。
终端一:
$ ./read_lock
read lock set by 5009
release lock by 5009
终端二:
$ ./read_lock
read lock set by 5010
release lock by 5010
读者可以将此结果与写入锁的运行结果相比较, 可以看出, 读取锁为共享锁, 当进程
5009 已设置读取锁后, 进程 5010 仍然可以设置读取锁。


3 多路复用

3.1 函数说明
    前面的 fcntl()函数解决了文件的共享问题, 接下来该处理 I/O 复用的情况了。
总的来说, I/O 处理的模型有 5 种。
1.阻塞 I/O 模型: 在这种模型下, 若所调用的 I/O 函数没有完成相关的功能, 则会使进程挂起, 直到相关数据到达才会返回。 如常见对管道设备、 终端设备和网络设备进行读写时经常会出现这种情况。


    2.非阻塞 I/O 模型: 在这种模型下, 当请求的 I/O 操作不能完成时, 则不让进程睡眠,而且立即返回。 非阻塞 I/O 使用户可以调用不会阻塞的 I/O 操作, 如 open()、 write()和 read()。 如果该操作不能完成, 则会立即返回出错(如打不开文件) 或者返回 0(如
在缓冲区中没有数据可以读取或者没空间可以写入数据)。

如何实现非阻塞IO访问:

O_NONBLOCK: 只能在打开的时候设置

fcntl: 可以在文件打开后设置


3.I/O 多路复用模型: 在这种模型下, 如果请求的 I/O 操作阻塞, 且它不是真正阻塞I/O, 而是让其中的一个函数等待, 在此期间, I/O 还能进行其他操作。 外部表现为阻塞式,内部非阻塞式自动轮询多路阻塞式IO。其实内部就是while加非阻塞(类似),但是它内部不会占用CPU 太多时间。

优点:不会浪费太多CPU时间,且达到while加非阻塞的效果

缺点:

1.select和poll本身是阻塞式的,当里面的条件没满足或者超时,就会一直阻塞,

2.这两个函数可以同时注册多个阻塞,但是只要有一个阻塞发生就会马上退出函数,而不会等其它阻塞发生,更不会等全部阻塞发生后才退出

 

4.信号驱动 I/O 模型: 在这种模型下, 进程要定义一个信号处理程序, 系统可以自动捕获特定信号的到来, 从而启动 I/O。 这是由内核通知用户何时可以启动一个 I/O操作决定的。它是非阻塞的。 当有就绪的数据时, 内核就向该进程发送 SIGIO 信号。 无论我们如何处理 SIGIO 信号, 这种模型的好处是当等待数据到达时, 可以不阻塞。 主程序继续执行,只有收到 SIGIO 信号时才去处理数据即可。


5.异步 I/O 模型: 在这种模型下, 进程先让内核启动 I/O 操作, 并在整个操作完成后通知该进程。 这种模型与信号驱动模型的主要区别在于: 信号驱动 I/O 是由内核通知我们何时可以启动一个 I/O 操作, 而异步 I/O 模型是由内核通知进程 I/O 操作何时完成的。 现在, 并不是所有的系统都支持这种模型。

涉及的函数:
(1)fcntl(F_GETFL、F_SETFL、O_ASYNC、F_SETOWN)设置文件的异步IO属性
(2)signal或者sigaction(SIGIO)注册异步IO处理函数
优点:1.类似中断处理,可以一直检测,
2.当一个鼠标阻塞发生,然后执行相应处理函数后,还可以继续检测鼠标阻塞,而不用手动又来重新添加一个鼠标阻塞,


        可以看到, select()和 poll()的 I/O 多路转接模型是处理 I/O 复用的一个高效的方法。 它可以具体设置程序中每一个所关心的文件描述符的条件、 希望等待的时间等, 从 select()和poll()函数返回时, 内核会通知用户已准备好的文件描述符的数量、 已准备好的条件(或事件) 等。 通过使用 select()和 poll()函数的返回结果(可能是检测到某个文件描述符的注册事件或是超时, 或是调用出错), 就可以调用相应的 I/O 处理函数了。


3.2 函数格式

select()函数的语法要点。

可以看到, select()函数根据希望进行的文件操作对文件描述符进行了分类处理, 这里对文件描述符的处理主要涉及 4 个宏函数

一般来说,

在每次使用 select()函数之, 首先使用 FD_ZERO()和 FD_SET()来初始化文件描述符集(在需要重复调用 select()函数时, 先把一次初始化好的文件描述符集备份下来,每次读取它即可)。

在 select()函数返回, 可循环使用 FD_ISSET()来检查描述符集, 在执行完对相关文件描述符的操作后, 使用 FD_CLR()来清除描述符集。

FD_ISSET(int fd, fd_set *set) 这个函数很有意思,当注册完一个文件后,再调用这个函数检查这个文件描述符,得到的值式非零,

当调用了select()后,再退出后,如果这个文件描述符的阻塞没有发生,则再调用这个函数就会返回0,如果发生了就会返回非0

可以用于检测阻塞有没有发生

另外, select()函数中的 timeout 是一个 struct timeval 类型的指针,该结构体如下所示:

struct timeval
{
    long tv_sec; /* 秒 */
    long tv_unsec; /* 微秒 */
}

可以看到, 这个时间结构体的精确度可以设置到微秒级, 这对于大多数的应用而言已经
足够了。
poll()函数的语法要点。

3.3 使用实例

        当使用 select()函数时, 存在一系列的问题, 例如, 内核必须检查多余的文件描述符,每次调用 select()之后必须重置被监听的文件描述符集, 而且可监听的文件个数受限制(使用 FD_SETSIZE 宏来表示 fd_set 结构能够容纳的文件描述符的最大数目) 等。 实际上,poll机制 比 select 机制相比效率更高, 使用范围更广。

select()函数的实例。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
int main(void)
{
	// 读取鼠标
	int fd = -1, ret = -1;
	char buf[200];
	fd_set myset;   //创建文件描述符集
	struct timeval tm;
	fd = open("/dev/input/mouse0", O_RDONLY);
	if (fd < 0)
	{
		perror("open:");
		return -1;
	}
	// 当前有2个fd,一共是fd一个是0
	// 处理myset
	FD_ZERO(&myset);     //初始化文件描述符集
	FD_SET(fd, &myset);  //添加文件描述
	FD_SET(0, &myset);  //添加文件描述
	if (FD_ISSET(0, &myset))
	{
		printf("键盘读出的内容是:.\n");
	}
	if (FD_ISSET(fd, &myset))
	{
		printf("鼠标读出的内容是:.\n");
	}
	tm.tv_sec = 5;
	tm.tv_usec = 0;
	ret = select(fd+1, &myset, NULL, NULL, &tm);
	if (ret < 0)
	{
		perror("select: ");
		return -1;
	}
	else if (ret == 0)
	{
		printf("超时了\n");
	}
	else
	{
		// 等到了一路IO,然后去监测到底是哪个IO到了,处理之
		if (FD_ISSET(0, &myset))
		{
			// 这里处理键盘
			memset(buf, 0, sizeof(buf));
			read(0, buf, 5);
			printf("键盘读出的内容是:[%s].\n", buf);
		}
		if (FD_ISSET(fd, &myset))
		{
			// 这里处理鼠标
			memset(buf, 0, sizeof(buf));
			read(fd, buf, 50);
			printf("鼠标读出的内容是:[%s].\n", buf);
		}
	}
	return 0;
}

poll()函数的实例。
    

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
int main(void)
{
	// 读取鼠标
	int fd = -1, ret = -1;
	char buf[200];
	struct pollfd myfds[2] = {0};
	fd = open("/dev/input/mouse0", O_RDONLY);
	if (fd < 0)
	{
		perror("open:");
		return -1;
	}
	// 初始化我们的pollfd
	myfds[0].fd = 0;			// 键盘
	myfds[0].events = POLLIN;	// 等待读操作
	myfds[1].fd = fd;			// 鼠标
	myfds[1].events = POLLIN;	// 等待读操作
	ret = poll(myfds, 2, 10000);
	if (ret < 0)
	{
		perror("poll: ");
		return -1;
	}
	else if (ret == 0)
	{
		printf("超时了\n");
	}
	else
	{
		// 等到了一路IO,然后去监测到底是哪个IO到了,处理之
		if (myfds[0].events == myfds[0].revents)
		{
			// 这里处理键盘
			memset(buf, 0, sizeof(buf));
			read(0, buf, 5);
			printf("键盘读出的内容是:[%s].\n", buf);
		}
		if (myfds[1].events == myfds[1].revents)
		{
			// 这里处理鼠标
			memset(buf, 0, sizeof(buf));
			read(fd, buf, 50);
			printf("鼠标读出的内容是:[%s].\n", buf);
		}
	}
	return 0;
}

 

 

 


 

  • 8
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值