七、文件IO——I/O处理方式和文件锁
7.1 I/O 处理方式
7.1.1 I/O处理的五种模型
- 阻塞I/O模型
- 若所调用的 I/O 函数没有完成相关的功能就会使进程挂起,直到相关数据到达才会返回。如 终端、网络设备的访问。
- 非阻塞模型
- 当请求的 I/O 操作不能完成时,则不让进程休眠,而且返回一个错误。如 open read write 访问
- I/O 多路转接模型
- 如果请求的 I/O 操作阻塞,且他不使真正阻塞 I/O,而且让其中一个函数等待,在这期间,I/O 还能进行其他操作。如 select 函数
- 信号驱动 I/O 模型
- 在这种模型下,通过安装一个信号处理程序,系统可以自动捕获特定信号的到来,从而启动 I/O
- 异步 I/O 模型
- 在这种模型下,当一个描述符已准备好,可以启动 I/O,进程会通知内核。由内核进行后续处理,这种用法现在较少
7.1.2 非阻塞I/O
- 低速系统调用时,进程可能会阻塞
- 非阻塞I/O确定操作(read, open, write)不阻塞,如果操作不能完成,则出错返回
- 设定非阻塞方式
- 使用 open 打开文件,设置 O_NONBLOCK 标志
- 如果一个文件已经打开,则使用 fcntl 修改文件状态标志为 非阻塞
7.1.3 例子
nonblock_read.c
1 /* 从标准输入读取信息,然后在屏幕上输出 2 * 测试: 3 * (1)在睡眠 5s 内 按ctrl + d 屏幕会输出 read finished 4 * ctrl + d 是给程序发送读取结束的信号,即读到了文件末尾 5 * (2)运行程序后,等待5s 输出 read error,程序不会阻塞在那里,没有输入,size < 0 6 * (3)在5 s 内输入字符,会正常输出字符 7 */ 8 9 #include <sys/types.h> 10 #include <sys/stat.h> 11 #include <fcntl.h> 12 #include <unistd.h> 13 #include <string.h> 14 #include <errno.h> 15 #include <stdlib.h> 16 #include <stdio.h> 17 #include <fcntl.h> 18 #include "io.h" 19 20 int main(int argc, const char *argv[]) 21 { 22 char buff[4096] = {'\0'}; 23 ssize_t size = 0; 24 25 //设置非阻塞 IO 26 sleep(5); 27 set_fl(STDIN_FILENO, O_NONBLOCK); 28 29 /* read 函数未设置 O_NONBLOCK, 默认是阻塞状态的 */ 30 size = read(STDIN_FILENO, buff, sizeof(buff)); 31 if(size < 0) { 32 perror("read error"); 33 exit(1); 34 } else if (size == 0) { 35 printf("read finished!!\n"); 36 } else { 37 if(write(STDOUT_FILENO, buff, size) != size) { 38 perror("write error"); 39 } 40 } 41 42 return 0; 43 }
7.2 文件锁
7.2.1 文件锁介绍
- 当多个用户共同使用、操作一个文件的时候,Linux 通常采用的方法是给文件上锁,来避免共享资源产生竞争的状态。
- 谁获得了锁,就可以对文件进行操作
- 文件锁按功能分为共享读锁 和独占写锁:
- 共享读锁:
- 文件描述符必须只读打开
- 一个进程上了读锁,其他进程也可以上读锁进行读取
- 独占写锁:
- 文件描述符必须只写打开
- 一个进程上了写锁,其他进程就不能上写锁和读锁进行读写操作
- 共享读锁:
- 文件锁按类型分为建议锁和强制性锁
- 建议性锁要求上锁文件的进程都要检测是否由锁存在,并尊重已由的锁
- 强制性锁由内核和系统执行的锁。文件挂载就是强制性锁
- fcntl 不仅可以实施建议性锁,而且可以实施强制性锁
这里用到 fcntl 函数
1 #include <unistd.h> 2 #include <fcntl.h> 3 int fcntl(int fd, int cmd, struct flock *lock);
cmd:F_SETLK F_GETLK F_SETLKW,前两种默认是非阻塞的,后面一种是阻塞时候用的
1 struct flock { 2 short l_type; /* F_RDLCK, F_WRLCK, or F_UNLCK */ 3 short l_whence; /* SEEK_SET, SEEK_CUR, SEEK_END */ 4 __kernel_off_t l_start; 5 __kernel_off_t l_len; 6 __kernel_pid_t l_pid; 7 __ARCH_FLOCK_PAD 8 9 };
- l_type:
- 锁类型,F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁一个区域)
- l_start、l_whence
- 要加锁或解锁的区域的起始地址,由 l_start 和 l_whence 两者决定
- l_start 是相对位移量,l_whence 则决定相对位移量的起点
- l_len
- 表示区域的长度
- 加锁解锁区域的注意点:
- 该区域可以在当前文件尾端处开始或越过其尾端处开始,但是不能在文件起始位置之前开始或越过该起始位置
- 若 l_len 为0,则表示锁的区域从其起点(由 l_start 和 l_whence 决定)开始直至最大可能位置为止。也就是不管添写到文件中多少数据,它都处于锁的范围
- 为了锁整个文件,通常的方法是将 l_start 设置为0,l_whence 设置为 SEEK_SET,l_len 设置为0
锁的继承和释放:
一个进程终止,它所建立的锁全部释放
关闭一个文件描述符,此进程对该文件的所有的锁均释放
子进程不继承父进程的锁
执行 exec 以后,新程序可以选择是否继承原来执行进程的锁
7.2.2 例子
两个进程进行相互排斥写
io.c
1 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <unistd.h> 5 #include "io.h" 6 #include <string.h> 7 #include <errno.h> 8 #include <stdlib.h> 9 #include <stdio.h> 10 #include <fcntl.h> 11 12 13 #define BUFFER_LEN 1024 14 15 /* 文件的读写拷贝 */ 16 void copy(int fdin, int fdout) 17 { 18 char buff[BUFFER_LEN]; 19 ssize_t size; 20 21 // printf("file length: %ld\n", lseek(fdin, 0L, SEEK_END));//将文件定位到文件尾部,偏移量为0L 22 // lseek(fdin, 0L, SEEK_SET);// 定位到文件开头 23 24 while((size = read(fdin, buff, BUFFER_LEN)) > 0) { //从 fdin 中读取 BUFFER_LEN 个字节存放入 buff 中 25 // printf("current: %ld\n", lseek(fdin, 0L, SEEK_CUR)); 26 27 if(write(fdout, buff, size) != size) { 28 fprintf(stderr, "write error: %s\n", strerror(errno)); 29 exit(1); 30 } 31 } 32 if(size < 0) { 33 fprintf(stderr, "read error:%s\n", strerror(errno)); 34 exit(1); // 相当于 return 1; 35 } 36 } 37 38 39 void set_fl(int fd, int flag) 40 { 41 int val; 42 43 //获得原来的文件状态标志 44 val = fcntl(fd, F_GETFL); 45 if(val < 0) { 46 perror("fcntl error"); 47 } 48 49 //增加新的文件状态标志 50 val |= flag; 51 52 //重新设置文件状态标志(val 为新的文件状态标志) 53 if(fcntl(fd, F_SETFL, val) < 0) { 54 perror("fcntl error"); 55 } 56 } 57 58 void clr_fl(int fd, int flag) 59 { 60 int val; 61 62 val = fcntl(fd, F_GETFL); 63 if(val < 0) { 64 perror("fcntl error"); 65 } 66 //清除指定的文件状态标志(设置为0) 67 val &= ~flag; 68 if(fcntl(fd, F_SETFL, val) < 0) { 69 perror("fcntl error"); 70 } 71 } 72 73 74 int lock_reg(int fd, int cmd, short type, off_t offset, short whence, off_t length) 75 { 76 struct flock flock; 77 flock.l_type = type; 78 flock.l_start = offset; 79 flock.l_whence = whence; 80 flock.l_len = length; 81 //flock.l_pid = getpid(); 82 //l_pid:加锁、解锁进程的进程号(pid) 83 84 if(fcntl(fd, cmd, &flock) < 0) { 85 perror("fcntl error"); 86 return 0; 87 } 88 89 return 1; 90 }
io.h
1 #ifndef __IO_H__ 2 #define __IO_H__ 3 4 #include <sys/types.h> 5 6 extern void copy(int fdin, int fdout); 7 8 extern void set_fl(int fd, int flag); 9 extern void clr_fl(int fd, int flag); 10 11 extern int lock_reg(int fd, int cmd, short type, off_t offset, short whence, off_t length); 12 13 /* 共享读锁,阻塞版本 */ 14 #define REAK_LOCKW(fd, offset, whence, length) \ 15 lock_reg(fd, F_SETLKW, F_RDLCK, offset, whence, length) 16 17 18 /* 共享读锁,非阻塞版本 */ 19 #define REAK_LOCK(fd, offset, whence, length) \ 20 lock_reg(fd, F_SETLK, F_RDLCK, offset, whence, length) 21 22 /* 独占写锁,阻塞版本 */ 23 #define WRITE_LOCKW(fd, offset, whence, length) \ 24 lock_reg(fd, F_SETLKW, F_WRLCK, offset, whence, length) 25 26 /* 独占写锁,非阻塞版本 */ 27 #define WRITE_LOC(fd, offset, whence, length) \ 28 lock_reg(fd, F_SETLK, F_WRLCK, offset, whence, length) 29 30 /* 解锁,非阻塞版本 */ 31 #define UNLOCK(fd, offset, whence, length) \ 32 lock_reg(fd, F_SETLK, F_UNLCK, offset, whence, length) 33 34 #endif
lock_write.c
1 #include <sys/types.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <unistd.h> 5 #include <string.h> 6 #include <errno.h> 7 #include <stdlib.h> 8 #include <stdio.h> 9 #include <fcntl.h> 10 #include "io.h" 11 12 int main(int argc, char *argv[]) 13 { 14 if(argc < 4) 15 { 16 printf("Usage: %s content file lock | unlock\n", argv[0]); 17 exit(1); 18 } 19 20 ssize_t size = strlen(argv[1]) * sizeof(char); 21 int fd = open(argv[2], O_WRONLY | O_CREAT, 0777); 22 if(fd < 0) { 23 perror("open error"); 24 exit(1); 25 } 26 27 sleep(5); 28 29 printf("current pid pid: %d\n", getpid()); 30 31 //如果要加锁,则加的是独占写锁,阻塞版本 32 //第二个进程想要对文件加文件锁(这里是独占写锁) 33 //必须要等前一个进程释放文件锁后方可加锁 34 if(!strcmp("lock", argv[3])) { 35 WRITE_LOCKW(fd, 0, SEEK_SET, 0);//整个文件上锁 36 printf("lock success\n"); 37 } 38 39 //写字符串 40 char *p = argv[1]; 41 int i; 42 for(i = 0; i < size; i++) { 43 if(write(fd, (p + i), 1) != 1) { 44 perror("write error"); 45 exit(1); 46 } 47 48 printf("%d success write one character\n", getpid()); 49 sleep(1); 50 } 51 52 //解锁 53 if(!strcmp("lock", argv[3])) { 54 UNLOCK(fd, 0, SEEK_SET, 0); 55 printf("unlock success\n"); 56 printf("unlock pid: %d", getpid()); 57 } 58 59 close(fd); 60 61 return 0; 62 }
编译,编写后台运行脚本
start.h
1 ./bin/lock_write aaaaaa demo.txt lock & 2 ./bin/lock_write AAAAAA demo.txt lock &
执行start.h 执行结果如下:
可以看见,一个进程对文件加锁之后,必须写完之后,并释放了锁之后,另一个进程才可以执行写操作。当一个进程执行完程序后,会自动释放锁,另一个进程再开始写,锁的释放完成是因为程序执行完或是 关闭了文件。
对共享读锁来说,一个进程加了锁,另一个进程也可以加锁,没影响。
修改下 start.h 脚本,让第二个进程不加锁
1 ./bin/lock_write aaaaaa demo.txt lock & 2 ./bin/lock_write AAAAAA demo.txt unlock &
运行脚本后的运行结果如下:
可以看到两个进程再交替进行写。第二个进程没有加锁,写代码区依然可以运行。
这种锁就是建议性锁,要写文件可以建议加锁区写,但是没加锁也可以写,当前Linux默认是这样做的。