1 引言:文件的读写冲突
如果两个或两个以上的进程同时向一个文件的某个特定区域写入数据,易产生混乱如下。
如果一个进程写,而其他进程同时读一个文件的某个特定区域,读出的数据极有可能因为读写操作的交错而不完整,如右图。
多个进程(多个文件表项,各自的读写位置)同时读一个文件的某个特定区域,不会有任何问题。它们只是各自把文件中的数据拷贝到各自的缓冲区中,并不会改变文件内容,相互之间也就不会冲突。
上图左,代码如下:
write.c //文件的写冲突示例
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
int main(int argc,char* argv[]){
// ./write hello
//打开文件
int fd = open("./shared.txt",O_WRONLY | O_CREAT | O_APPEND,0664);
if(fd == -1){
perror("open");
return -1;
}
//向文件中写入数据
for(int i = 0;i < strlen(argv[1]);i++){
write(fd,&argv[1][i],sizeof(argv[1][i]));
sleep(1);
}
//关闭文件
close(fd);
return 0;
}
//两个窗口,一个执行./write hello,另一个执行./write world
//尽量同时执行,查看文件,不是helloworld,而是hweolrllod
由此得出结论,为了避免在写、读写同一个文件的的同一区域时发生冲突,进程之间应该遵循以下规则:
如果一个程序正在写,那么其他程序既不能写也不能读。
如果一个程序正在读,那么其他程序不能写,但可以读。
2 文件锁
2.1 理论
为了避免以上冲突,Unix/Linux系统引入文件锁机制,并把文件锁分为读锁和写锁两种。
读锁:对一个文件的特定区域可以加多把读锁
写锁:对一个文件的特定区域职能加一把写锁
基于锁的操作模型顺序:
1)上读/写锁
2)读/写文件的特定区域
3)解锁
文件锁的加锁操作:
假设进程A期望访问某文件的A区,同时进程B期望访问该文件的B区,而AB区存在部分重叠,分4种情况讨论:
情况1:进程A正在写,进程B也想写:
情况2:进程A正在写,进程B想读:
情况3:进程A正在读,进程B却想写:
情况4:进程A正在读,进程B也想读
2.2 fcntl()
#include <fcntl.h>
int fcntl(int fd, F_SETLK/F_SETLKW, struct flock* lock);
功能:加解锁
F_SETLK:非阻塞模式加锁
F_SETLKW:阻塞模式加锁
返回值:成0败-1
struct flock{
short l_type; //锁类型:F_RDLCK / F_WRLCK / F_UNLCK
short l_whence; //锁区偏移起点:SEEK_SET / SEEK_CUR / SEEK_END
off_t l_start; // 锁区偏移字节数,0表示文件头
off_t l_len; //锁区字节数,0表示一直锁到文件尾
pid_t l_pid; //加锁进程的PID,-1表示自动设置
};
通过对该结构体类型变量的赋值,再配合fcntl函数,以完成对文件指定区域的加解锁操作。
//wlock.c 文件锁
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
int main(int argc,char* argv[]){
// ./write hello
//打开文件
int fd = open("./shared.txt",O_WRONLY | O_CREAT | O_APPEND,0664);
if(fd == -1){
perror("open");
return -1;
}
//以阻塞方式加锁
struct flock l;
l.l_type = F_WRLCK;//写锁
l.l_whence = SEEK_SET;
l.l_start = 0;
l.l_len = 0;//一直锁到文件尾
l.l_pid = -1;
//阻塞方式加锁
/*if(fcntl(fd,F_SETLKW,&l) == -1){
perror("fcntl");
return -1;
}*/
//非阻塞方式加锁
while(fcntl(fd,F_SETLK,&l) == -1){
printf("加不上,干点别的去....\n");
sleep(1);
}
//向文件中写入数据
for(int i = 0;i < strlen(argv[1]);i++){
write(fd,&argv[1][i],sizeof(argv[1][i]));
sleep(1);
}
//解锁
struct flock ul;
ul.l_type = F_UNLCK;//解锁
ul.l_whence = SEEK_SET;
ul.l_start = 0;
ul.l_len = 0;//一直解到文件尾
ul.l_pid = -1;
if(fcntl(fd,F_SETLK,&ul) == -1){
perror("fcntl");
return -1;
}
//关闭文件
close(fd);
return 0;
}
//两个窗口,一个执行./wlock hello,另一个执行./wlock world
//尽量同时执行,查看文件,是helloworld,文件锁成功
2.3 强调
文件加锁是以非阻塞方式执行的。
当通过close()函数关闭文件描述符时,调用进程在该fd上所加的一切锁将被自动解除。
当进程终止时,该进程在所有fd上所加的一切锁将被自动解除。
文件锁仅在不同进程间起作用,同一个进程的不同线程不能通过文件锁解决读写冲突问题。
通过fork / vfork函数创建的子进程,不继承父进程所加的任何文件锁。
通过exec函数创建的新进程,会继承原进程所加的全部文件锁,除非某fd带有FD_CLOEXEC标志。
2.4 劝谏锁 / 协议锁
从前述基于锁的操作模型可以看出,锁机制之所以能够避免读写冲突,关键在于参与读写的多个进程都在按照一套模式——先加锁,再读写,最后解锁——按部就班地进行。
这就形成了一套协议,只要参与者无一例外地遵循这套协议(程序员在每个可执行文件代码中都写了fcntl()文件锁相关代码),读写就是安全的。
反之,如果哪个进程不遵守这套协议(程序员在某个可执行文件代码中未加fcntl()文件锁代码),想读就读,想写就写,即便其他进程有锁,对它起不到任何约束作用。
因此,这样的锁机制被称为劝谏锁或协议锁。
3 文件锁的内核结构
每次对给定文件的特定区域加锁,都会通过fcntl函数向系统内核传递flock结构体,该结构体中包含了有关锁的一切细节,诸如锁的类型(读锁/写锁),锁区的起始位置和大小,甚至加锁进程的PID(填-1由系统自动设置)。
系统内核会收集所有进程对该文件所加的各种锁,并把这些flock结构体中的信息,以链表的形式组织成一张锁表,而锁表的起始地址就保存在该文件的v节点中。
任何一个进程通过fcntl函数对该文件加锁,系统内核都要遍历这张锁表,一旦发现冲突的锁,立即阻塞或报错,否则将欲加之锁插入锁表。而解锁的过程实际上就是调整或删除锁表中的响应节点。
4 文件的元数据
4.1 stat()
#include <sys/stat.h>
int stat(char const* path, struct stat* buf);
int fstat(int fd, struct stat* buf);
int lstat(char const* path, struct stat* buf);
功能:从i节点中提取文件的元数据,即文件的属性信息
path:文件路径
buf: 文件元数据结构
fd: 文件描述符
返回值:成0败-1
lstat()函数与另外两个函数的区别在于它不跟踪符号链接。例如。
abc.txt --> xyz.txt abc.txt文件时xyz.txt文件的符号链接
stat("abc.txt" , ...) //得到xyz.txt文件的元数据
lstat("abc.txt" , ...) //得到abc.txt文件的元数据
stat函数族通过stat结构体,向调用者输出文件的元数据。
struct stat{
dev_t st_dev; //设备ID
ino_t st_ino; //i节点号
mode_t st_mode; //文件的类型和权限
nlink_t st_nlink; //硬链接数
uid_t st_uid; //拥有者用户ID
gid_t st_gid; //拥有者组ID
dev_t st_rdev; //特殊设备ID
off_t st_size; //总字节数
blksize_t st_blksize; //I/O块字节数
blkcnt_t st_blocks; //存储块数
time_t st_atime; //最后访问时间
time_t st_mtime; //最后修改时间
time_t st_ctime; //最后状态改变时间
}
stat结构的st_mode成员表示文件的类型和权限,该成员在stat结构中被声明为mode_t类型,其原始类型在32位系统中被定义为unsigned int,即32位无符号整数,但到目前为止,只有低16位有意义。
用16位二进制数(B15...B0)表示的文件类型和权限,从高到低可被分为5组:
B15 - B12:文件类型
B11 - B9 :设置用户ID、设置组ID、粘滞
B8 - B6 :拥有者用户的读、写和执行权限
B5 - B3 :拥有者组的读、写和执行权限
B2 - B0 :其他用户的读、写和执行权限
B15 - B12:文件类型:
B11 - B9 :设置用户ID、设置组ID、粘滞
系统中的每个进程其实都有两个用户ID:实际用户ID,有效用户ID
进程的实际用户ID继承其父进程的实际用户ID。当一个用户通过合法的用户名和
口令登录系统以后,系统就会为它启动一个Shell进程,Shell进程的实际用户ID就是该
登录用户的用户ID。该用户在Shell下启动的任何进程都是Shell进程的子进程,自然也
就继承了Shell进程的实际用户ID。
一个进程的用户身份决定了它可以访问哪些资源,比如读、写或执行某个文件。但
真正被用于权限验证的并不是进程的实际用户ID,而是其有效用户ID。
一般情况下,进程的有效用户ID取自其实际用户ID,可以认为二者等价。
但是,如果用户启动该进程的可执行文件带有设置用户ID位,即B11位为1,那么该
进程的有效用户ID就不再取自其实际用户ID,而是取自可执行文件的拥有者用户ID。
系统管理员常用这种方法提升普通用户的权限,让他们有能力去完成一些本来只有
root用户才能完成的任务。例如,他可以为某个拥有者用户为root的可执行文件添加设置
用户ID位,这样一来,无论运行这个可执行文件的实际用户是谁,启动起来的进程的有
效用户ID都是root,凡是root用户可以访问的资源,该进程都可以访问。当然,具体访问
哪些资源,以何种方式访问,还要由这个可执行文件的代码来决定。作为一个安全的操
作系统,不可能允许一个低权限用户在高权限状态下为所欲为,如通过passwd命令修改
口令。
带有设置用户ID位的不可执行文件:没有意义。
带有设置用户ID位的目录文件:没有意义。
与设置用户ID位的情况类似,如果一个可执行文件带有设置组ID位,即B10位为1,
那么运行该可执行文件所得到的的进程,它的有效组ID同样不取自其实际组ID,而是取
自其可执行文件的拥有者组ID。
带有设置组ID位的不可执行文件:某些系统上用这种无意义的组合表示强制锁。
带有设置组ID位的目录文件:在该目录下创建文件或子目录,其拥有者组取自该
目录的拥有者组,而非创建者用户所隶属的组。
带有粘滞位(B9)的可执行文件,在其首次运行并结束后,其代码区被连续地保存
在磁盘交换区中,而一般磁盘文件的数据块是离散存放的。因此,下次运行该程序可以
获得较快的载入速度。
带有粘滞位(B9)的不可执行文件:没有意义。
带有粘滞位(B9)的目录:除root以外的任何用户在该目录下,都只能删除或者更名
那些属于自己的文件或子目录。而对于其他用户的文件或子目录,既不能删除也不能
改名,如/tmp目录。
B8 - B6 :拥有者用户的读、写和执行权限:
B5 - B3 :拥有者组的读、写和执行权限:
B2 - B0 :其他用户的读、写和执行权限:
辅助分析文件类型的实用宏:
S_ISREG() 是否普通文件
S_ISDIR() 是否目录
S_ISSOCK() 是否本地套接字
S_ISCHR() 是否字符设备
S_ISBLK() 是否块设备
S_ISLNK() 是否符号链接
S_ISFIFO() 是否有名管道
//stat.c 获取文件的元数据
#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<time.h>
// hello.c --> stat() --> struct stat s --> s.st_mode --> mtos() --> -rwxrwxr-x
char* mtos(mode_t m){
static char s[11];
if(S_ISDIR(m)){
strcpy(s,"d");
}else
if(S_ISSOCK(m)){
strcpy(s,"s");
}else
if(S_ISCHR(m)){
strcpy(s,"c");
}else
if(S_ISBLK(m)){
strcpy(s,"b");
}else
if(S_ISLNK(m)){
strcpy(s,"l");
}else
if(S_ISFIFO(m)){
strcpy(s,"p");
}else {
strcpy(s,"-");
}
strcat(s,m & S_IRUSR ? "r" : "-");
strcat(s,m & S_IWUSR ? "w" : "-");
strcat(s,m & S_IXUSR ? "x" : "-");
strcat(s,m & S_IRGRP ? "r" : "-");
strcat(s,m & S_IWGRP ? "w" : "-");
strcat(s,m & S_IXGRP ? "x" : "-");
strcat(s,m & S_IROTH ? "r" : "-");
strcat(s,m & S_IWOTH ? "w" : "-");
strcat(s,m & S_IXOTH ? "x" : "-");
return s;
}
//时间转换
char* ttos(time_t t){
static char time[20];
struct tm* l = localtime(&t);
sprintf(time,"%d-%d-%d %d:%d:%d",l->tm_year + 1900,l->tm_mon + 1,l->tm_mday,
l->tm_hour,l->tm_min,l->tm_sec);
return time;
}
int main(int argc,char* argv[]){
// ./a.out hello.c
struct stat s;//用来输出文件的元数据信息
if(stat(argv[1],&s) == -1){
perror("stat");
return -1;
}
printf(" 设备ID:%lu\n",s.st_dev);
printf(" i节点号:%ld\n",s.st_ino);
printf(" 类型和权限:%s\n",mtos(s.st_mode));
printf(" 硬链接数:%lu\n",s.st_nlink);
printf(" 用户ID:%u\n",s.st_uid);
printf(" 组ID:%u\n",s.st_gid);
printf(" 特殊设备ID:%lu\n",s.st_rdev);
printf(" 总字节数:%ld\n",s.st_size);
printf(" IO块字节数:%ld\n",s.st_blksize);
printf(" 存储块数:%ld\n",s.st_blocks);
printf(" 最后访问时间:%s\n",ttos(s.st_atime));
printf(" 最后修改时间:%s\n",ttos(s.st_mtime));
printf(" 最后改变时间:%s\n",ttos(s.st_ctime));
return 0;
}
5 内存映射文件
5.1 mmap()
#include <sys/mman.h>
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
功能:建立虚拟内存到物理内存或磁盘文件的映射;
start:映射区虚拟内存的起始地址,NULL系统自动选定后返回。
length:映射区字节数,自动按页圆整。
prot:映射区操作权限,可取以下值:
PROT_READ 映射区可读
PROT_WRITE 映射区可写
PROT_EXEC 映射区可执行
PROT_NONE 映射区不可访问
flags:映射标志,可取以下值
MAP_ANONYMOUS 匿名映射,将虚拟内存映射到物理内存而非文件,
忽略fd和offset参数
MAP_PRIVATE 对映射区的写操作只反映到缓冲区,并不会真正写入文件
写操作结束后,才由缓冲区写入文件(...)
MAP_SHARED 对映射区的写操作直接反映到文件中(2必选1)
MAP_DENYWRITE 拒绝其它对文件的写操作
MAP_FIXED 若在start上无法创建映射,则失败(无此标志系统会自动调整)
fd:文件描述符
offset:偏移量,以页为单位,0从第1个字节开始,4096从第4097个开始。
0代表前面有0个,4096代表前面4096个 0.0
返回值:成功返回映射区虚拟地址的起始地址,失败返回MAP_FAILED((void*)-1)
//fmap.c 内存映射文件
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/mman.h>// mmap() munmap()
int main(void){
//打开文件
int fd = open("./fmap.txt",O_RDWR|O_CREAT|O_TRUNC,0664);
if(fd == -1){
perror("open");
return -1;
}
//修改文件大小!!!尝试不写此段,报总线错误(核心已转储),txt文件0字节
if(ftruncate(fd,4096) == -1){
perror("ftruncate");
return -1;
}
//建立虚拟地址到磁盘文件的映射 //默认void*,就可以任何类型接,这里选char*类型接
char* start = mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(start == MAP_FAILED){
perror("mmap");
return -1;
}
//操作文件
strcpy(start,"我要吃梨");// 相当于write(但write更专业,不需要前述ftruncate)
printf("%s\n",start);// 相当于read
//解除映射关系
if(munmap(start,4096) == -1){
perror("munmap");
return -1;
}
//关闭文件
close(fd);
return 0;
}
//编译执行,查看txt文件 小鸡炖蘑菇^@^@^@^@^@^@^@^@^@^@^@
mmap()可映射到物理内存中,就是标准库函数malloc()的底层。
mmap()可映射到磁盘文件中,速度最快(而write() read()会涉及多次复制),可进程间通信 。
5.2 munmap()
#include<sys/mman.h>
int munmap(void* start, size_t length);
功能:解除虚拟内存到物理内存或磁盘文件的映射
start:映射区虚拟内存的起始地址
length:映射区字节数,自动按页圆整
返回值:成0败-1
munmap()允许对映射区的一部分解映射,但必须按页处理