Linux数据管理初探
Linux数据管理初探
- 导语
- 内存管理
- 内存分配
- 内存错用和处理
- 文件锁定
- 锁文件/区域锁
- 读写和竞争
- 锁命令和死锁
- dbm数据库
- 例程
- dbm访问函数
- 其他dbm函数
- 总结
- 参考文献
导语
Linux为应用程序提供简洁的视图用来反映可直接寻址的内存空间(但实际上可能是内存+外存),并且Linux提供了内存保护机制(如遇到访问越界自动终止等),当Linux被正确配置后,它提供给用户逻辑上的空间会比实际的空间更大(例如交换-覆盖技术和虚拟存储器技术)
内存管理
内存分配
最基础的内存分配是通过C语言的malloc实现的(C语言基础内容,不多解释),它返回一个void指针,使用的时候需要强转,并且malloc保证返回的地址是内存对齐的
由于例如交换-覆盖和虚拟存储器这类技术的存在,Linux程序在运行时往往可以获得更多比机器物理内存容量更多的内存,书上给出了一个例子,由于笔者机器内存是4GB,因此修改了相关参数,具体代码和运行结果如下
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
int *locate=0;
for(int i=0;i<2048;i++)//总共申请了8GB
{
locate=malloc(1024*1024*sizeof(int));//每次申请4MB
if(locate)
printf("%d times success",i);
else
exit(EXIT_FAILURE);
}
return 0;
}
可以看到一直到第2048次申请仍然是有效的,这充分说明了Linux程序是可以申请到比内存更大的空间的
书上给出了一个更极端的例子,这里只给出解析和代码,由于考虑到可能对虚拟机有影响,因此不运行
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
int* locate=0;
int obtain=0;
while(1)
{
for(int i=0;i<1024;i++)
{
locate=malloc(1024*sizeof(int));//每次4KB
if(!locate)exit(EXIT_FAILURE);
}//每次4MB
obtained++;
printf("%d times malloc\n",obtained);
}
return 0;
}
在预想中,该程序的运行会花费很多时间,并且在分配内存大小接近机器物理内存容量,分配的速度会慢,这是因为程序在不同阶段使用的内存是不一样的(空闲物理内存->交换系统),它们是用请求分页虚拟存储系统实现的,具体相关知识可以参考操作系统的考研课
除了malloc之外,还有一些其他的与内存相关的函数,原型如下
void free(void *ptr_to_memory)//释放指定的内存空间
void *calloc(size_t number_of_elements, size_t element_size);
//为结构数组分配内存,默认初始化0
void *realloc(void *existing_memory, size_t new_size);
//改变已经分配好的内存长度
内存错用和处理
在具体的内存分配使用过程中,通常会出现越界和写/读空这样两种错误,例如下面两个例子
//越界
int *p=malloc(1024);
while(1)
{
*p=0;
p++;
}
//写/读空
char *p=0;
printf("%s",p);
sprintf(p,"write");
第一个例子,循环会到达超过初始分配空间的地址,Linux的内存管理系统会检测到越界行为并终止程序运行,第二个例子,程序段尝试对一个空指针进行读写,同样会被检测到不能读写而终止
文件锁定
程序经常会需要对数据的权限进行开放或限制,这通常由文件来实现,Linux提供对文件“上锁”和“解锁”的原子操作,而且还可以对文件的某一部分进行上锁或解锁,进行更细微的操作,这里可以参考操作系统中文件互斥和信号量相关的知识点
锁文件/区域锁
锁文件只是充当一个指示器,程序间需要相互协作,锁文件只是建议锁而非强制锁,强制锁会对读写权限进行控制,下面是书上给出的一个例子和运行结果
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
int flag=open("/tmp/test",O_RDWR | O_CREAT | O_EXCL,0444);
if(flag!=-1)printf("Succeed\n");//创建并上锁成功
else printf("Open failed with error %d\n",errno);//输出错误代码
return 0;
}
可以看到,第一次运行时由于文件不存在,open调用成功,第二次则返回错误码17代表已存在
书上还给出临界区以及进程之间相互竞争临界区的实现,具体代码和结果如下
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
const char *lock_file="/tmp/LCK.test2";//操作的文件
int main()
{
int t=10;
while(t--)
{
int flag=open(lock_file,O_RDWR | O_CREAT | O_EXCL, 0444);//创建文件
if(flag==-1)//如果申请失败代表正在使用
{
printf("%d - using\n",getpid());//并不是当前进程在使用
sleep(3);
}
else//临界区
{
printf("%d -access\n",getpid());//当前进程拿到了锁
sleep(1);
close(flag);//关文件
unlink(lock_file);//解锁
sleep(2);
}
}
return 0;
}
可以看到两个进程交互访问临界区,并且当一个进程使用时,另一个进程会等待一会后再次尝试访问临界区
上面的两个例子都是使用文件锁,而不是对文件的某一部分上锁,创建锁文件适用于独占式访问,但是并不适用访问大型的共享文件(例如缓冲区),当出现类似生产者-消费者模型的问题时,锁文件就不适用了,因为一个文件可能有多个区域要被同时更新和访问,如果不及时更新,数据就会丢失,这个时候,区域锁就应运而生
区域锁实现了将文件某个特定部分锁定,然而其他程序可以访问文件中的其他部分(文件段锁定或文件区域锁定),Linux使用fcntl和lockf来实现这一功能(一般用前者,后者是备用的,并且两者由于实现不同不能同时使用),fcntl原型如下
int fcntl(int fildes, int command, ...);
//对打开的文件描述符操作,根据command完成不同任务
int fcntl(int fildes, int command, struct flock *flock_structure);
//command为F_GETLK、F_SETLK、F_SETLKW时的函数时候的fcntl,第三个参数为文件锁
//文件锁结构体具体的成员和取值略,有三种模式,强制共享,解锁,强制私有
F_GETLK获取文件锁信息,不会去尝试锁定文件,在该命令条件下,即使传递了想创建的锁类型,fcntl也会阻止;F_SETLK试图上锁或解锁,对应区域由flock的相关成员变量定义;F_SETLKW是前者的循环版本,当无法获取锁时,调用会等待知道可以(获取锁或收到信号),除了上述的三种,还需要注意的是,程序对某个文件拥有的所有锁都将在相应文件描述符被关闭时自动清除,程序结束时也是
读写和竞争
对文件区域上锁后,就只能用read/write调用而不是fread/fwrite来访问数据,因为后者会对数据进行缓存,实际每次读到的数据会超过目标长度,但如果后面对超过的部分修改,修改的数据并不能及时更新到缓存当中,就会产生读写问题
关于共享锁和私有锁的操作,书上给出了例子,下面是代码和对应的运行结果(先后台运行第一个,再运行第二个)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
const char *test_file = "/tmp/test_lock";
int main() {
struct flock rg1, rg2;
int file = open(test_file, O_RDWR | O_CREAT, 0666);//打开文件
if (file < 0) {
fprintf(stderr, "fail open\n");
return 0;
}
for (int i = 0; i < 200; i++)//写入数据
(void)write(file, "A", 1);
rg1.l_type = F_RDLCK;//第一个文件区域
rg1.l_whence = SEEK_SET;
rg1.l_start = 10;
rg1.l_len = 20;
rg2.l_type = F_WRLCK;//第二个文件区域
rg2.l_whence = SEEK_SET;
rg2.l_start = 40;
rg2.l_len = 10;
printf("%d locking\n", getpid());
printf("Attempting to set rg1 lock...\n");
int flag = fcntl(file, F_SETLK, &rg1);//区域1上锁
if (flag == -1) {
fprintf(stderr, "rg1 lock failed\n");
} else {
printf("rg1 lock succeeded\n");
}
printf("Attempting to set rg2 lock...\n");
flag = fcntl(file, F_SETLK, &rg2);//区域2上锁
if (flag == -1) {
fprintf(stderr, "rg2 lock failed\n");
} else {
printf("rg2 lock succeeded\n");
}
sleep(60); // 增加睡眠时间,方便测试
printf("%d closing\n", getpid());
close(file);
return 0;
}
下面是第二个代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
const char *test_file = "/tmp/test_lock";
void show_lock_info(struct flock *rg) {//展示信息函数
printf("\tl_type %d, ", rg->l_type);
printf("l_whence %d, ", rg->l_whence);
printf("l_start %ld, ", rg->l_start);
printf("l_len %ld, ", rg->l_len);
printf("l_pid %d\n", rg->l_pid);
}
int main() {
struct flock rg;
int file = open(test_file, O_RDWR | O_CREAT, 0666);
if (file < 0) {//如果打不开
fprintf(stderr, "fail open\n");
return 0;
}
for (int i = 0; i < 99; i += 5) {//每次测试5个长度
rg.l_type = F_WRLCK;
rg.l_whence = SEEK_SET;
rg.l_start = i;
rg.l_len = 5;
rg.l_pid = -1;
printf("Testing %d to %d, F_WRLCK\n", i, i + 5);
int flag = fcntl(file, F_GETLK, &rg);//查看锁信息
if (flag == -1) {
fprintf(stderr, "F_GETLK failed\n");
return 0;
}
if (rg.l_pid != -1) {
printf("lock would fail, F_GETLK returned:\n");
show_lock_info(&rg);
} else {
printf("F_WRLCK would succeed\n");//如果上共享锁会成功
}
rg.l_type = F_RDLCK;
rg.l_whence = SEEK_SET;
rg.l_start = i;
rg.l_len = 5;
rg.l_pid = -1;
printf("Testing %d to %d, F_RDLCK\n", i, i + 5);
flag = fcntl(file, F_GETLK, &rg);
if (flag == -1) {
fprintf(stderr, "F_GETLK failed\n");
return 0;
}
if (rg.l_pid != -1) {
printf("lock would fail, F_GETLK returned:\n");
show_lock_info(&rg);
} else {
printf("F_RDLCK would succeed\n");//如果上独享锁会成功
}
}
close(file);
return 0;
}
运行结果如下,可以看到对10-30上独占锁会失败,对40-50上共享锁会失败
锁命令和死锁
lockf函数也可以实现对文件的锁定,具体原型如下
int lockf(int fildes, int function, off_t size_to_lock);
//只能操作独占锁,size_to_lock是操作的字节数,从文件当前偏移开始算
/*
F_ULOCK解锁,F_LOCK独占锁
F_TLOCK测试并独占锁,F_TEST测试其他进程设置的锁
*/
lockf和fcntl一样,所有的锁都是建议锁,并不会真正地阻止读取文件中的数据,对锁的检测是程序的责任,但是两者是不能混合使用的
死锁的概念在OS中经常提及,它通常被认为是进程之间资源的分配/申请不当而导致的一种相互等待的状况,书上给出的死锁对象是程序,这当然也可以。当许多用户频繁访问同一个数据时很容易发生死锁,但是Linux内核不能自动解开死锁,这时候就需要一些外部干涉手段,具体可以参考死锁的四个避免原理
dbm数据库
dbm是Linux自带的数据库,它适合存储相对静态的索引化数据库,在使用它之前需要确保当前系统安装了gdbm和ndbm库,并且在编译对应文件时需要加上-lgdbm_compat选项
例程
dbm的基本元素是需要存储的数据块以及与其关联的在检索数据时用作关键字的数据块(键值对),这些通过datum来实现,datum至少包括两个成员变量dptr和dsize,前者是void*类型,它指向数据的起始点,后者是size_t类型,它是包含数据的长度,无论是对待存储或是用来访问它的索引都是用datum实现
dbm访问函数
主要的dbm函数原型和解释如下
DBM *dbm_open(const char *filename, int file_open_flags, mode_t file_mode);
//打开已有数据库或创建数据库,其余参数类似open
int dbm_store(DBM *database ,datum key ,datum content, int store_mode);
//把数据存在数据库中,数据库索引和实际索引,对已有关键字的操作(覆盖或失败)
datum dbm_fetck(DBM *database, datum key);
//检索数据,返回一个datum类型
void dbm_close(DBM *database);
//关闭dbm_open打开的数据库
这里给出一个基于书上代码修改的程序和运行结果,具体解析如下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <gdbm.h>
#include <string.h>
#define TEST_DB_FILE "/tmp/dbm1_test"
typedef struct test_data {//设置的数据
char misc_chars[20];
int num;
char more_chars[20];
} node;
node store[3], get;
char key[20];
datum kd, dd;
GDBM_FILE dbm_ptr;
int main() {
// 打开数据库
dbm_ptr = gdbm_open(TEST_DB_FILE, 0, GDBM_WRCREAT, 0666, NULL);
if (dbm_ptr == NULL) {
perror("gdbm_open");
return 1;
}
// 初始化数据
strcpy(store[0].misc_chars, "first!"); // 小写
store[0].num = 47;
strcpy(store[0].more_chars, "foo");
strcpy(store[1].misc_chars, "bar"); // 小写
store[1].num = 13;
strcpy(store[1].more_chars, "unlucky?");
strcpy(store[2].misc_chars, "third"); // 小写
store[2].num = 3;
strcpy(store[2].more_chars, "baz");
// 存储数据到数据库
for (int i = 0; i < 3; i++) {
sprintf(key, "%c%c%d", store[i].misc_chars[0], store[i].more_chars[0], store[i].num);
//生成的关键字为第一个字符串的首字符+第三个字符串首字符+数字
kd.dptr = key;
kd.dsize = strlen(key);
// 分配空间并将结构体数据复制到字符数组
char *data_buffer = malloc(sizeof(node));
memcpy(data_buffer, &store[i], sizeof(node));
dd.dptr = data_buffer; // 指向字符数组
dd.dsize = sizeof(node);
// 存储数据,使用替换模式
if (gdbm_store(dbm_ptr, kd, dd, GDBM_REPLACE) < 0) {
perror("gdbm_store");
free(data_buffer); // 释放分配的内存
}
}
// 从数据库中检索数据
sprintf(key, "bu%d", 13); // 使用小写
kd.dptr = key;
kd.dsize = strlen(key);
dd = gdbm_fetch(dbm_ptr, kd);//根据关键字找
if (dd.dptr) {
printf("Data retrieved\n");
memcpy(&get, dd.dptr, dd.dsize);
printf("Retrieved item - %s %d %s\n", get.misc_chars, get.num, get.more_chars);
free(dd.dptr); // 释放获取的数据
} else {
printf("No data found for key: %s\n", key);
}
// 关闭数据库
gdbm_close(dbm_ptr);
return 0;
}
可以看到成功检索到数据,但需要注意的是,如果先前运行出的结果不对,需要把临时文件删除,不然会影响到第二次运行
其他dbm函数
除了上一节的函数,还有一些仅供检测状态和删除的函数,函数原型和解释如下
int dbm_delete(DBM *database_descriptor, datum key);
//从数据库删除数据项,成功时返回0
int dbm_error(DBM *database_descriptor);
//测试数据库是否有错误,没有就返回0
int dbm_clearerr(DBM *database_descriptor);
//清除数据库所有已1的错误条件标志
datum dbm_firstkey(DBM *database_descriptor);
datum dbm_nextkey(DBM *database_descriptor);
//类似于C++的迭代器,第一个是获得第一个数据项,第二个是获得下一个位置
/*
for(datum key=dbm_firstkey(db_ptr);key.dptr;key=dbm_nextkey(db_ptr));
*/
基于书上给出的代码修改和运行结果如下(只给出修改部分)
// 尝试删除数据
sprintf(key, "bu%d", 13); // 使用小写
kd.dptr = key;
kd.dsize = strlen(key);
if (gdbm_delete(dbm_ptr, kd) == 0) {
printf("Data with key %s deleted\n", key);
} else {
printf("Nothing deleted for key %s\n", key);
}
// 遍历数据库中的所有键并打印数据
//这里用了gdbm,原理和dbm一样
for (kd = gdbm_firstkey(dbm_ptr); kd.dptr; kd = gdbm_nextkey(dbm_ptr, kd)) {
dd = gdbm_fetch(dbm_ptr, kd);
if (dd.dptr) {
printf("Data retrieved for key: %s\n", (char *)kd.dptr); // 打印当前键
memcpy(&get, dd.dptr, dd.dsize);
printf("Retrieved item - %s %d %s\n", get.misc_chars, get.num, get.more_chars);
free(dd.dptr); // 释放获取的数据
} else {
printf("No data found for key: %s\n", (char *)kd.dptr);
}
}
// 关闭数据库
gdbm_close(dbm_ptr);
return 0;
可以看到输入的数据被删除,遍历数据库时只输出了存在的数据
总结
这一章简述了Linux系统下对文件的各种权限操作以及自带的数据库dbm/gdbm,许多操作系统的经典问题得以用文件锁来实现,一些相对静态的索引化数据也可以用dbm/gdbm来使用
参考文献
- 《Linux程序设计(第四版)》