Linux程序设计(19)第七章:数据管理(1)内存管理

1. 内存管理

1.1 分配大量内存

耗尽机器内存测试(centos7 虚拟机环境下 分配1G内存 实际分配2.7G内存)

#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#define ONE_K (1024)

int main() {
	char * some_memory ;
	int size_to_allocate = ONE_K;
	int megs_obtained = 0;
	int ks_obtained = 0;
	
	while (1) {
		for(ks_obtained = 0; ks_obtained < 1024; ks_obtained++) {
			some_memory = (char * )malloc (size_to_allocate);
			if (some_memory == NULL) 
				exit(1);
			sprintf(some_memory, "Hello World");
		}
		
		megs_obtained++;
		printf("Now allocated %d Megabytes\n", megs_obtained);
	}
	exit(0);
}

结果:

[root@localhost linux-]# ./a.out 
Now allocated 1 Megabytes
Now allocated 2 Megabytes
……
Now allocated 2711 Megabytes
Now allocated 2712 Megabytes
Now allocated 2713 Megabytes
Now allocated 2714 Megabytes
Killed
[root@localhost linux-]#

程序还是分配了大大超出机器 物理内存容量的内存。最后,系统为了保护自己的安全运行,终止了这个贪婪的程序。

1.2 交换空间

Linux实现了一个“按需换页的虚拟内存系统"。
用户程序看到的所有内存 全是虚拟的,也就是说,它并不真正存在于程序使用的物理地址上。Linux将所有的内存都以页为单 位进行划分,通常每一页的大小为4096字节。每当程序试图访问内存时,就会发生虚拟内存到物理内 存的转换,转换的具体实现和耗费的时间取决于你所使用的特定硬件情况。当所访问的内存在物理上并不存在时,就会产生一个页面错误并将控制权交给内核。

Linux内核会对访问的内存地址进行检查,如果这个地址对于程序来说是合法可用的,内核就会 确定需要向程序提供哪一个物理内存页面。然后,如果该页面之前从未被写入过,内核就直接分配它, 如果它已经被保存在硬盘的交换空间上,内核就读取包含数据的内存页面到物理内存(可能需要把• 个已有页面从内存中移出到硬盘)。接着,在完成虚拟内存地址到物理地址的映射之后,内核允许用 户程序继续运行。Linux应用程序并不需要操心这一过程,因为所有的具体实现都已隐藏在内核中了。

1.3 虚拟内存

参考:
虚拟地址空间
https://blog.csdn.net/lqy971966/article/details/119378416

1.4 OOM

内存耗尽(OOM)

1.5 内存滥用

#include <stdlib.h>

#define ONE_K (1024)
int main() {
	char *some_memory;
	char *scan_ptr;
	some_memory = (char *)malloc(ONE_K); 
	if (some_memory == NULL) 
		exit(1);
	scan_ptr = some_memory; 
	while(1) {
		*scan_ptr = '\0';
		scan_ptr++;
	}
	
	exit(0);
}

结果:

[root@localhost linux-]# gcc mem3.c 
[root@localhost linux-]# ./a.out 
Segmentation fault (core dumped)
[root@localhost linux-]# 

Linux内存管理系统能保护系统的其他部分免受这种内存滥用的影响。
为了确保一个行为恶劣的 程序(如本例)无法破化任何其他程序,Linux会终止其运行。

1.6 空指针

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
	char *some_memory = (char *)0;
	printf("A read from null %s\n", some_memory); 
	sprintf(some_memory, "A write to null\n"); 
	exit(0);
}

结果:

[root@localhost linux-]# ./a.out 
A read from null (null)
Segmentation fault (core dumped)
[root@localhost linux-]#

Linux (在GNU C函数库的包装下)容忍了读操作,它只输出一个包含(null) \0 的“魔术”字符串。
但对于写操作就没有如此宽容了,它直接终止了该程序

1.7 realloc calloc

参考:
calloc,malloc 和 realloc
https://blog.csdn.net/lqy971966/article/details/117983301

2. 文件锁定

背景:
程序经常需要共享数据,而这 通常是通过文件来实现的。因此,对于这些程序来说,建立某种控制文件的方式就非常重要了。只有 这样,文件才可以通过一种安全的方式更新,或者说,当一个程序正在对文件进行写操作时,文件就 会进入一个暂时状态,在这个状态下如果另外一个程序尝试读这个文件,它就会自动停下来等待这 个状态的结束。

2.1 原子操作锁文件(全局锁)和区域锁文件

Linux提供了多种特性来实现文件锁定。
其中最简单的方法就是以原f操作的方式创建锁文件, 所谓“原子操作”就是在创建锁文件时,系统将不允许任何其他的事情发生。这就给程序提供了一 种方式来确保它所创建的文件是唯一的,而且这个文件不可能被其他程序在同一时刻创建。

第二种方法更高级一些,它允许程序锁定文件的一部分,从而可以独享对这一部分内容的访问。 有两种不同的方式可以实现第二种形式的文件锁定。我们将只对其中的一种做详细介绍,因为两种方式非常相似一第二种方式只不过是程序接口稍微不同而已。

2.1.1 创建锁文件

许多应用程序只需要能够针对某个资源创建一个锁文件即可。然后,其他程序就可以通过检查这个文件来判断它们自己是否被允许访问这个资源。
这些锁文件通常都被放置在一个特定位置,并带有一个与被控制资源相关的文件名。

锁文件仅仅只是充当一个指示器的角色,程序间需要通过相互协作来使用它们。

2.1.2 open 锁文件例子

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<errno.h>

int main()
{
	int file_desc;
	int save_errno;
	file_desc = open("/tmp/LCK.test", O_RDWR | O_CREAT | O_EXCL, 0444); 
	if (file_desc == -1) {
		save_errno = errno;
		printf ("Open failed with error %d\n", save_errno);
	}else {
		printf("Open succeeded\n");
	}
	exit(0);
}

结果:

[root@localhost linux-]# ./a.out 
Open succeeded
[root@localhost linux-]# ./a.out 
Open failed with error 17
[root@localhost linux-]#

这个程序调用带有O_CREAT和O_EXCL标志的open来创建文件/tmp/LCK.test第一次运行程序 时,由于文件并不存在,所以open调用成功。但对程序的后续调用失败了,因为文件已经存在了。
错误号17代表的是 EXIST,这个错误用来表示一个文件已存在。
#define EXIST 17 /* File exists */

2.2 锁文件

2.2.1 协调性锁文件

#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 file_desc;
	int tries = 5;
	while (tries--) {
		file_desc = open(lock_file, O_RDWR | O_CREAT | O_EXCL, 0444); 
		if (file_desc == -1) {
			printf("%d - Lock already present\n", getpid());
			sleep(3);
		}else {
		//临界区从这里开始:
		printf("%d - I have exclusive access\n", getpid()); 
		sleep(1);
		(void)close(file_desc);
		(void)unlink(lock_file);
		//在这里结束:
		sleep(2);
		}
	}
	
	exit(0);
}

结果:

[root@localhost linux-]# ./a.out  & ./a.out 
[1] 16828
16829 - I have exclusive access
16828 - Lock already present
16828 - I have exclusive access
16829 - Lock already present
16829 - I have exclusive access
16828 - Lock already present
16829 - I have exclusive access
16828 - Lock already present
16828 - I have exclusive access
16829 - Lock already present
[1]+  Done                    ./a.out
[root@localhost linux-]# 

这个程序然后通过创建一个唯一的锁文件 /tmp/LCK.test2 来访问临界资源。
如果因为文件已存在而失败,程序将等候一小段时间后再次尝试。
如果成功,它就可以开始访问资源。在标记为“临界区”的部分,你可以执行任何需要独占式访问的处理。
因为这只是一个演示程序,所以你只等待了一小段时间。程序使用完资源后,它将通过删除锁文 件来释放锁。然后它可以在重新申请锁之前执行一些其他的处理(本例中只是调用sleep函数)。
这里锁文件扮演了类似二进制信号量的角色,就问题“我可以使用这个资源吗? ”给每个程序一个“是” 或“否”的答案。

2.3 区域锁(文件段锁定)

2.3.1 背景:

创建锁文件的方法并不适用于访问大型的共享文件。
假设你有一个大文件,它由一个程序写入数据,但却由许多不同的程序同时对这个文件进行更新。当一个程序负责记录长期以来连续收集到的数据,而其他一些程序负责对记录的数据进行处理时,这种情况就可能发生。处理程序不能等待记录程序结束,因为记录程序将一直不停地运行,所以它们需要一些协调方法来提供对同一个文件的并发访问。

你可以通过锁定文件区域的方法来解决这个问题,文件中的某个特定部分被锁定了,但其他程序可以访问这个文件中的其他部分。这被称为文件段锁定或文件区域锁定。

2.3.2 两种方式(fcntl 和 lockf)

Linux提供了至少两种方式
来实现这一功能:使用fcntl系统调用和使用lockf调用
我们将主要介绍fcntl接口,因为它是最常使用的接口。lockf和fcntsl非常相似,在Linux中,它一般作为fcntl的备选接口。
但是,fcntl和lockf 的锁定机制不能同时工作,它们使用不同的底层实现,因此决不要混合使用这两种类型的调用,而应 坚持使用其中的一种。

2.4 fcntl 例子

2.4.1 fcntl 必须使用 read write 系统调用

对文件区域加锁之后,你必须使用底层的read和write调用来访问文件中的数据,而不要使用更高级的fread和fwrite调用。
这是因为fread和fwrite会对读写的数据进行缓存,所以执行一次fread调用来读取文件中的头100个字节可能(事实上,是儿乎肯定如此)会读取超过100个字节的数据,并将多余的数据在函数库中进行缓存。如果程序再次使用fread来读取下100个字节的数据,它实际上将读取已缓冲在函数库中的数据,而不会引发一个底层的read调用来从文件中取出更多的数据。

例子说明:
为了说明这为什么是一个问题,让我们来考虑这样一个例子:
两个程序都打算更新同一个文件。 假设这个文件由200个全为零的字节组成。第一个程序先开始运行,并获得该文件头100个字节的写锁。它然后使用fread来读取这100个字节。但是正如我们在前面章节中所看到的,fread会一次读取多达buf个字节的数据,因此,它实际上把整个文件都读到了内存中,但仅把头100个字节传递给程序。
接着,第二个程序开始运行。它获得了文件后100个字节的写锁。这个操作将会成功,因为第一个程序只锁定了文件的前100个字节。第二个程序将100-199字节的数据都写成2,关闭文件并解锁,最后退出程序。这时,第一个程序锁定了文件的后100个字节,然后调用fread来读取数据。尽管真正存在于文件中的数据是100个字节的2,但是因为先前数据已经被缓存,所以程序实际上读到的数据将是100个字节的零。但如果你使用read和write,这个问题就不会发生。

2.4.2 代码例子

程序首先创建一个文件,并以可读可写方式打开它,然后再在文件中添加一些数据。
接着在文 件中设置两个区域:
第一个区域为10-30字节,使用共享(读)锁;
第二个区域为40-50字节,使用独占(写)锁。
然后程序调用fcntl来锁定这两个区域,并在关闭文件和退出程序前等待一分钟。

fcntl1.c

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>

const char *test_file = "/tmp/test_lock";

int main()
{
		int file_desc;
		int byte_count;
		char *byte_to_write = "A";
		struct flock region_l;
		struct flock region_2;
		int res;

		//打开一个文件描述符:
		file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
		if (!file_desc) {
			fprintf(stderr, "Unable to open %s for read/write\n", test_file);
			exit(1);
		}

		//给文件添加一些数据:
		for(byte_count = 0; byte_count < 100; byte_count++) {
			(void)write(file_desc, byte_to_write, 1); 
		}

		//(4)把文件的10-30字节设为区域1,并在其上设置共享锁:
		region_l.l_type = F_RDLCK;
		region_l.l_whence = SEEK_SET;
		region_l.l_start = 10;
		region_l.l_len = 20;

		//(5)把文件的40-50字节设为区域2,并在其上设置独占锁:
		region_2.l_type = F_WRLCK ;
		region_2.l_whence = SEEK_SET;
		region_2.l_start = 40;
		region_2.l_len = 10;

		//(6)现在锁定文件:
		printf("Process %d locking file\n", getpid());
		
		res = fcntl(file_desc, F_SETLK, &region_l);
		if (res == -1) 
			fprintf(stderr, "Failed to lock region l\n");

		res = fcntl(file_desc, F_SETLK, &region_2);
		if (res == -1) 
			fprintf(stderr, "Failed to lock region 2\n");

		//(7)然后等一会儿:
		sleep(60);
		printf("Process %d closing file\n", getpid());
		close(file_desc);
		exit(0);
}

fcntl2.c

#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<fcntl.h>

const char *test_file = "/tmp/test_lock";
#define SIZE_TO_TRY 5

void show_lock_info(struct flock *to_show) {
	printf("l_type %d, ", to_show->l_type); 
	printf("l_whence %d, ", to_show->l_whence); 
	printf("l_start %d, ", (int)to_show->l_start); 
	printf("l_len %d, ", (int)to_show->l_len); 
	printf("l_pid %d\n", to_show->l_pid);
}

int main() {
	int file_desc;
	int res;
	struct flock region_to_test; 
	int start_byte;
	
	//(2)打开一个文件描述符:
	file_desc = open(test_file, O_RDWR | O_CREAT, 0666); 
	if (!file_desc) {
		fprintf(stderr, "Unable to open %s for read/write\n", test_file); 
		exit(1);
	}
	
	for (start_byte = 0; start_byte < 99; start_byte += SIZE_TO_TRY) {
		//(3)设置希望测试的文件区域:
		region_to_test.l_type = F_WRLCK;
		region_to_test.l_whence = SEEK_SET;
		region_to_test.l_start = start_byte;
		region_to_test.l_len = SIZE_TO_TRY;
		region_to_test.l_pid = -1;
		printf("Testing F_WRLCK on region from %d to %d\n", start_byte, start_byte + SIZE_TO_TRY);
		
		//(4)现在测试文件上的锁:
		res = fcntl(file_desc, F_GETLK, &region_to_test);
		if (res == -1) {
			fprintf(stderr, "F_GETLK failed\n"); exit(EXIT_FAILURE);
		}
		
		if (region_to_test.l_pid != -1) {
			printf("Lock would fail. F_GETLK returned:\n"); 
			show_lock_info(&region_to_test);
		}else{
			printf("F_WRLCK - Lock would succeed\n");
		}
		
		//(5)用共享(读)锁重复测试一次,再次设置希望测试的文件区域:
		region_to_test.l_type = F_RDLCK;
		region_to_test.l_whence = SEEK_SET;
		region_to_test.l_start = start_byte;
		region_to_test.l_len = SIZE_TO_TRY;
		region_to_test.l_pid = -1;
		
		printf("Testing F_RDLCK on region from %d to %d\n", start_byte, start_byte + SIZE_TO_TRY);
		
		//(6)再次测试文件上的锁:
		res = fcntl(file_desc, F_GETLK, &region_to_test);
		if (res == -1) { 
			fprintf(stderr, "F_GETLK failed\n"); 
			exit(1);
		}
		
		if (region_to_test.l_pid != -1) {
			printf("Lock would fail. F_GETLK returned:\n"); 
			show_lock_info(&region_to_test);
		}else {
			printf("F_RDLCK - Lock would succeed\n");
		}
	}
	close(file_desc);
	exit(0);
}	

结果:

[root@localhost linux-chegnxusheji]# gcc fcntl2.c -o 4
[root@localhost linux-chegnxusheji]# ./3 &
[1] 3209
[root@localhost linux-chegnxusheji]# Process 3209 locking file

[root@localhost linux-chegnxusheji]# ./4
Testing F_WRLCK on region from 0 to 5
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 0 to 5
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 5 to 10
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 5 to 10
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 10 to 15
Lock would fail. F_GETLK returned:
l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 3209
Testing F_RDLCK on region from 10 to 15
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 15 to 20
Lock would fail. F_GETLK returned:
l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 3209
Testing F_RDLCK on region from 15 to 20
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 20 to 25
Lock would fail. F_GETLK returned:
l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 3209
Testing F_RDLCK on region from 20 to 25
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 25 to 30
Lock would fail. F_GETLK returned:
l_type 0, l_whence 0, l_start 10, l_len 20, l_pid 3209
Testing F_RDLCK on region from 25 to 30
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 30 to 35
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 30 to 35
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 35 to 40
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 35 to 40
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 40 to 45
Lock would fail. F_GETLK returned:
l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 3209
Testing F_RDLCK on region from 40 to 45
Lock would fail. F_GETLK returned:
l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 3209
Testing F_WRLCK on region from 45 to 50
Lock would fail. F_GETLK returned:
l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 3209
Testing F_RDLCK on region from 45 to 50
Lock would fail. F_GETLK returned:
l_type 1, l_whence 0, l_start 40, l_len 10, l_pid 3209
Testing F_WRLCK on region from 50 to 55
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 50 to 55
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 55 to 60
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 55 to 60
F_RDLCK - Lock would succeed
……
F_RDLCK - Lock would succeed
Testing F_WRLCK on region from 95 to 100
F_WRLCK - Lock would succeed
Testing F_RDLCK on region from 95 to 100
F_RDLCK - Lock would succeed
[root@localhost linux-chegnxusheji]#

说明:
每5个字节来分别测试读锁和写锁。
l_pid初始化为-1,如果已经被设置了读锁,则返回0;写锁则返回1;

2.5 文件锁的竞争

两个程序争夺文件上同一区域上的锁会发生什么情况。
依旧利用 fcntl1.c 的程序,然后通过 fcntl3.c 的程序对同有个文件进行读锁,写锁获取。

fcntl3.c 程序如下:

2.6 其他锁命令 lockf

2.7 死锁

死锁就是所有线程都在相互等待释放资源,导致谁也无法继续执行下去。

参考:
多线程(4)什么是锁?锁机制,死锁等说明
https://blog.csdn.net/lqy971966/article/details/104527787

3. dbm 数据库

おすすめ

転載: blog.csdn.net/lqy971966/article/details/120846396