系列文章:
介绍资源分配的管理方式(动态内存管理),介绍如何对可能被多个用户同时访问的文件进行处理(文件锁定)。
内存管理
简单的内存分配
使用标准C语言函数库中的malloc调用来分配内存:
#include <stdlib.h>
void *malloc(size_t size);
用来指定待分配内存字节数量的参数size不是一个简单的整型,虽然通常是一个无符号整型。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define A_MEGABYTE (1024 * 1024)
int main()
{
char *some_memory;
int megabyte = A_MEGABYTE;
int exit_code = EXIT_FAILURE;
some_memory = (char *)malloc(megabyte);
if(some_memory != NULL)
{
sprintf(some_memory, "Hello, World!\n");
printf("%s", some_memory);
exit_code = EXIT_SUCCESS;
}
exit(exit_code);
}
这个程序要求malloc函数给它返回一个指向1MB内存空间的指针。首先检查malloc函数被成功调用,然后通过使用其中的部分内存来表明分配的内存确实已经存在。当运行这个程序时,可以看到程序输出Hello World,这表明malloc确实返回了1MB的可用内存。
由于mallo函数返回的是一个 void* 指针,因此需要通过类型转换,将其转换至你需要的 char* 类型。malloc 函数可以保证其返回的内存是地址对齐的,所以它可以被转换为任何类型的指针。
分配大量的内存
请求全部的物理内存
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#define A_MEGABYTE (1024 * 1024)
#define PHY_MEM_MEGS 1024
int main()
{
char *some_memory;
size_t size_t_allocate = A_MEGABYTE;
int megs_obtained = 0;
while(megs_obtained < (PHY_MEM_MEGS * 16))
{
some_memory = (char *)malloc(size_t_allocate);
if(some_memory != NULL)
{
megs_obtained++;
sprintf(some_memory, "Hello World");
printf("%s - now allocated %d Megabytes\n", some_memory, megs_obtained);
}
else
{
exit(EXIT_FAILURE);
}
}
exit(EXIT_SUCCESS);
}
通过循环来不断申请越来越多的内存,直到它已分配了在PHY_MEM_MEGS中定义的物理内存容量的2倍以上。这个程序似乎耗尽了机器上物理内存中的每个字节,但是出乎意料这个程序运行良好。
每次申请1K内存并在获得的每个内存块上写入数据
#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(EXIT_FAILURE);
sprintf(some_memory, "Hello World");
}
megs_obtained++;
printf("Now allocated %d Megabytes \n", megs_obtained);
}
exit(EXIT_FAILURE);
}
...
Now allocated 4217 Megabytes
Now allocated 4218 Megabytes
Now allocated 4219 Megabytes
...
当分配的内存大小接近机器物理内存容量时,速度明显变慢,而且你能很明显地感觉到硬盘的操作。但这个程序还是分配了大大超过机器物理内存容量的内存,最后,系统为了保护自己的安全运行,终止这个贪婪的程序。
应用程序所分配的内存是由Linux内核管理的。每次请求内存或者尝试读写它已经分配的内存时,便会由Linux内核接管并决定如何处理这些请求。
刚开始时,内核只是通过使用空闲的物理内存来满足应用程序的内存请求,但是当物理内存耗尽时,它便会开始使用交换空间。与Windows不同,Linux的交换空间中没有局部堆、全局堆或可丢弃内存段等需要在代码中操心的内容 —— Linux 内核会为你完成所有的管理工作。
内核会在物理内存和交换空间之间移动数据和程序代码,使得每次读写内存时,数据看起来总像是已存在于物理内存中,而不管你在访问它们之前,它们在哪里。
用更专业的术语来说,Linux 实现了一个“按需换页的虚拟内存系统”。用户程序看到的所有内存全是虚拟的,也就是说,它不真正存在于程序使用的物理地址上。Linux 将所有的内存都以页为单位进行划分,通常一页的大小为 4096 字节。每当程序试图访问内存时,就会发生虚拟内存倒物理内存的转换,转换的具体实现和耗费的时间取决于你所使用的特定硬件情况。当所访问的内存在物理上并不存在时,就会产生一个页面错误并将控制权交给内核。
Linux 内核会对访问的内存地址进行检查,如果这个地址对于程序来说是合法可用的,内核就会确定需要向程序提供哪一个物理内存页面。然后,如果该页面之前从未被写入过,内核就直接分配给它,如果它已经被保存在硬盘的交换空间上,内核就读取包含数据的内存页面到物理内存(可能需要把一个已有页面从内存中移出到硬盘)。接着,在完成虚拟内存地址到物理地址的映射之后,内核允许用户程序继续运行。Linux 应用程序并不需要操心这一过程,因为所有的具体实现都已隐藏在内核中了。
最终,当应用程序耗尽所有的物理内存和交换空间,或者当最大栈长度被超过时,内核将拒绝最后的内存请求,并可能提前终止程序的运行。
滥用内存
先分配一些内存,然后尝试在它之后写些数据。
#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(EXIT_FAILURE);
scan_ptr = some_memory;
while (1)
{
*scan_ptr = '\0';
scan_ptr++;
}
exit(EXIT_SUCCESS);
}
程序运行结果:Segmentation fault
Linux 内存管理系统能保护系统的其他部分免受这种内存滥用的影响。为了确保一个行为恶劣的程序无法破坏任何其它程序,Linux会终止其运行。
每个Linux系统中运行的程序都只能看到属于自己的内存映像,不同的程序看到的内存映像不同。只有操作系统知道物理内存是如何安排的,它不为用户程序管理内存,同时也为用户程序提供彼此之间的隔离保护。
空指针
访问空指针情况。
#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(EXIT_SUCCESS);
}
第一个printf 函数试图打印一个取自空指针的字符串,接着sprintf 函数尝试向一个空指针里写数据。在本例中,Linux 在 GNU C函数库的容忍下,执行了读操作,它只输出一个包含(NULL) 的魔术字符串,但是对写操作,它直接终止了程序。
这次不使用GNU C 函数库,你将会发现从零地址处读数据也是不被允许的。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
char z = *(const char *) 0;
printf("I read from location zero\n");
}
这次,直接尝试从零地址处读数据,而且这次和内核之间并没有GNU lib库的存在,于是,程序被终止了。
释放内存
Linux 内存管理系统完全有能力在程序结束时,将申请的内存返还给系统。但是,如果程序不会结束呢?这需要我们根据需要动态地使用内存。
动态使用内存的程序应该总是通过 free 调用,把不同的内存释放给malloc内存管理器。这样做可以将分散的内存块重新整合到一起,并由malloc函数库来管理而非应用程序。
如果一个运行中的进程自己使用并释放内存,则这些自由内存实际上仍然处于被分配给该进程的状态。在幕后,Linux 将程序员使用的内存块作为一个物理页面管理,通常内存中的每个页面为4K字节。但如果一个内存页面未被使用,Linux 内存管理器就可以将其从物理内存置换到交换空间中,从而减轻它对资源使用的影响。如果程序试图访问位于已置换到交换空间中的内存页中的数据,那么Linux会短暂地暂停程序,将内存页从交换空间再次置换到物理内存,然后允许程序继续运行,就像数据一直存在于内存中一样。
#include <stdlib.h>
void free(void *ptr_to_memory);
调用 free 时使用的指针参数必须是指向由 malloc、calloc 或 realloc 调用所分配的内存。
#include <stdlib.h>
#include <stdio.h>
#define ONE_K (1024)
int main()
{
char *some_memory;
int exit_code = EXIT_FAILURE;
some_memory = (char *)malloc(ONE_K);
if(some_memory != NULL)
{
free(some_memory);
printf("Memory allocated and freed again\n");
exit_code = EXIT_SUCCESS;
}
exit(exit_code);
}
一旦调用了free释放了一块内存,它就不再属于这个进程,它将由mallloc函数库负责管理。在对一块内存调用free之后,就不能再进行读写操作了。
其它的内存分配函数 —— calloc、realloc
#include <stdlib.h>
void *calloc(size_t number_of_elements, size_t element_size);
void *realloc(void *existing_memory, size_t new_size);
虽然calloc分配的内存也可以用free来释放,但它的参数与malloc有所不同。它的作用是为一个结构数组分配内存,因此需要把元素个数和每个元素的大小作为其参数。它所分配的内存将全部初始化为0。如果calloc调用成功,它返回指向数组中第一个元素的指针。与malloc调用类似,后续的calloc调用无法保证能返回一个连续的内存空间,因此不能通过重复调用 calloc,并期望第二个调用返回的内存正好接在第一个调用返回的内存之后来扩大calloc调用创建的数组。
realloc 函数用来改变先前已经分配的内存块长度,他需要传递一个指向先前通过malloc、calloc 或 realloc 调用分配的内存的指针,然后根据new_size参数的值来增加或减少其长度。为了完成这一任务,realloc 函数可能不得不移动数据,因此特别重要的一点是,你要确保一旦内存被重新分配之后,你必须使用新的指针而不是使用realloc调用前的那个指针去访问内存。
另一个需要注意的问题是,如果 realloc 无法调整内存块大小的话,它会返回一个null指针。
避免使用下面的代码:
my_ptr = malloc(BLOCK_SIZE);
...
my_ptr = realloc(my_ptr, BLOCK_SIZE * 10);
如果realloc调用失败,它将返回一个空指针,my_ptr 就将指向null,而先前用malloc分配的内存将无法再通过my_ptr进行访问。因此,在释放老内存块之前,最好的方法是先用malloc请求一块新内存,再通过memcpy把数据从老内存块复制到新的内存块。这样即使出现错误,应用程序还是可以继续访问存储在原来内存块中的数据,从而能够实现一个干净的程序终止。
文件锁定
文件锁是多用户、多任务操作系统中一个非常重要的组成部分。程序经常需要共享数据,而这通过文件来实现的。因此,对于这些程序来说,建立某种控制文件的方式就非常重要了。只有这样,文件才可以通过一种安全的方式更新,或者说,当一个程序正在对文件进行写操作时,文件就会进入一个暂时状态,在这个状态下,如果另外一个程序尝试读这个文件,它就会自动停下来等待这个状态的结束。
Linux 提供了多种特性来实现文件锁定。
最简单的方法就是以原子操作的方式创建文件锁,所谓原子操作就是在创建锁文件时,系统将不允许任何其他的事情发生。这就给程序提供了一种方式来确保它所创建的文件是唯一的,而这个文件不可能被其它程序在同一时刻创建。
第二种方式,允许锁文件的一部分,从而可以独享这一部分内容的访问。
创建锁文件
许多应用程序只需要能够针对某个资源创建一个锁文件即可。然后,其他程序就可以通过检查这个文件来判断它们自己是否被允许访问这个资源。
这些锁文件通常被放置在一个特定位置,并带有一个与被控制资源相关的文件名。
锁文件仅仅只是充当一个指示器的角色,程序间需要通过相互协作来使用它们。用术语来说,锁文件只是建议锁,而不是强制锁,在后者中,系统将强制锁的行为。
为了创建一个用作锁指示器的文件,你可以使用在 fcntl.h 头文件中定义的 open 系统调用,并带上 O_CREAT 和O_EXCL 标志,这样能够以一个原子操作同时完成两项工作:确定文件不存在,然后创建它。
#include <unistd.h>
#include <stdlib.h>
#include <stdio.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(EXIT_SUCCESS);
}
这个程序调用带有 O_CREAT 和 O_EXCL 标志的 open 来创建 /tmp/LCK.text 。第一次运行程序时,由于文件并不存在,所有 open 调用成功,但对程序的后续调用都失败了,因此文件已经存在了。如果想让程序再次执行成功,你必须删除那个锁文件。
至少在Linux系统中,错误代号17代表的是EEXIST,这个错误用来表示一个文件已存在。错误号定义在头文件 errno.h 中。这个错误号实际定义在头文件 /usr/include/asm-generic/errno-base.h 。
如果一个程序在它执行时,只需独占某个资源一段很短的时间 —— 临界区,它就需要在进入临界区之前使用 open 系统调用创建锁文件,然后在退出临界区时用 unlink 系统调用删除该锁文件。
协调性锁文件。
#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 = 10;
while(tries--)
{
file_desc = open(lock_file, O_RDWR | O_CREAT | O_CREAT, 0444);
if(file_desc == -1)
{
printf("%d - Lock already present\n", getpid());
sleep(3);
}
else
{
printf("%d - I have exclusive access\n", getegid());
sleep(1);
(void)close(file_desc);
(void)unlink(lock_file);
sleep(2);
}
}
exit(EXIT_SUCCESS);
}
这里的锁文件扮演了类似二进制信号量的角色。
区域锁定
用创建锁文件的方法来控制对诸如串行口或不经常访问的文件之类的资源的独占式访问,是一个不错的选择,但它并不适用于访问大型的共享文件。加入你有一个大文件,它由一个程序写入数据,但却由许多不同的程序同时对这个文件进行更新。当一个程序负责记录长期以来连续收集到的数据,而其他一些程序负责对记录的数据进行处理时,这种情况就有可能发生。处理程序不能等待记录程序结束,因为记录程序将一直不停地运行,所以它们需要一些协调方法来提供对同一个文件的并发访问。
可以通过锁定文件区域的方法来解决这个问题,文件中的某个特定部分被锁定了,但其他程序可以访问这个文件中的其他部分。这被称为文件段锁定或文件区域锁定。Linux 提供了至少两种方式来实现这一功能:fcntl 系统调用和使用lockf调用,不能混合使用二者。
#include <fcntl.h>
int fcntl(int fildes, int command, ...);
fcntl 对一个打开的文件描述符进行操作,并能根据 command 参数的设置完成不同的任务。它为我们提供了 3 个用于文件锁定的命令选项:
当使用这些命令选项时,fcntl 的第三个参数必须是一个指向 flock 结构的指针,所以实际的函数原型为:
int fcntl(int fildes, int command, struct flock * flock_structure);
flock(文件锁)结构依赖具体的实现,至少包含下述成员:
- short l_type
- short l_whence
- off_t l_start
- off_t l_len
- off_t l_pid
l_type 成员的取值定义在 fcntl.h 中,
取值 | 说明 |
---|
F_RDLCK | 共享(或读)锁。许多不同的进程可以拥有同一(或者重叠)区域上的共享锁。只要任一进程拥有一把共享锁,那么就没有进程可以再获得该区域上的独占锁。 | F_UNLCK | 解锁 | F_WRLCK | 独占(写)锁。只有一个进程可以在文件的任一特定区域拥有一把独占锁。一旦一个进程拥有了这样一把锁,任何其它进程都无法在该区域上获得任何类型的锁。为了获得一把独占锁,文件必须以写或读/写方式打开 |
l_whence、l_start、l_len 三者定义了文件中的一个区域,即一个连续的字节集合。l_whence的取值必须是 SEEK_SET、SEEK_CUR、SEEK_END中的一个。它们分别对应于文件头、当前位置和文件尾。l_whence 定义了 l_start 的相对偏移值,其中,l_start 是该区域的第一个字节。l_whence 通常被设为 SEEK_SET,这时 l_start 就从文件的开始计算。l_len 参数定义了该区域的字节数。
l_pid 参数用来记录持有锁的进程。
1. F_GETLK
它用于获取 fildes (第一个参数)打开的文件的锁信息。它不会尝试去锁定文件。调用进程把自己想创建的锁类型信息传递给 fcntl,使用 F_GETLK 命令的 fcntl 就会返回将会阻止获取锁的任何信息。
进程可能使用 F_GETLK 调用来查看文件中某个区域的当前锁状态。它应该设置 flock 结构来表明它需要的锁类型,并定义它感兴趣的文件区域。fcntl 调用如果成功就返回非 -1 的值。如果文件已被锁定从而阻止锁请求成功执行,fcntl 会用相关信息覆盖 flock 结构。如果锁请求可以成功执行,flock 结构将保持不变。如果 F_GETLK调用无法获得信息,它将返回 -1 表明失败。
如果 F_GETLK 调用成功,调用程序就必须检查 flock 结构的内容来判断其是否被修改过。因为 l_pid 的值被设置成持有锁的进程的标识符,所以通过检查这个字段就可以很方便地判断出 flock 结构是否被修改过。
2. F_SETLK
这个命令试图对 fildes 指向的文件的某个区域加锁或者解锁。
与F_GETLK一样,要加锁的区域由flock结构中的l_start、l_whence、l_len的值定义。如果加锁成功,fcntl 将返回一个非 -1 的值,如果失败,则返回-1。
3. F_SETLKW
与 F_SETLK 命令作用相同,但在无法获取锁时,这个调用将等待直到可以为止。一旦这个调用开始等待,只有在可以获取锁或收到一个信号时它才会返回。
程序对某个文件拥有的所有锁都将在相应的文件描述符被关闭时自动清除。在程序结束时也会自动清除各种锁。
锁定状态下的读写操作
当对文件区域加锁之后,你必须使用底层的read和write调用来访问文件中的数据,而不要使用更高级的fread和fwrite调用,这是因为二者会对读写的数据进行缓存。
使用 fcntl 锁定文件
#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_1;
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(EXIT_FAILURE);
}
for(byte_count = 0; byte_count < 100; byte_count++)
{
(void)write(file_desc, byte_to_write, 1);
}
region_1.l_type = F_RDLCK;
region_1.l_whence = SEEK_SET;
region_1.l_start = 10;
region_1.l_len = 20;
region_2.l_type = F_WRLCK;
region_2.l_whence = SEEK_SET;
region_2.l_start = 40;
region_2.l_len = 10;
printf("Process %d locking file\n", getpid());
res = fcntl(file_desc, F_SETLK, ®ion_1);
if(res == -1) fprintf(stderr, "Failed to lock region 1\n");
res = fcntl(file_desc, F_SETLK, ®ion_2);
if(res == -1) fprintf(stderr, "Failed to lock region 2\n");
}
#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);
int main()
{
int file_desc;
int res;
struct flock region_to_test;
int start_byte;
file_desc = open(test_file, O_RDWR | O_CREAT, 0666);
if(!file_desc)
{
fprintf(stderr, "Unable to open %s for read/write", test_file);
exit(EXIT_FAILURE);
}
for(start_byte = 0; start_byte < 99; start_byte += SIZE_TO_TRY)
{
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);
res = fcntl(file_desc, F_GETLK, ®ion_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(®ion_to_test);
}
else
{
printf("F_WRLCK - Lock would succeed\n");
}
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);
res = fcntl(file_desc, F_GETLK, ®ion_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(®ion_to_test);
}
else
{
printf("F_RDLCK - Lock would succeed\n");
}
}
close(file_desc);
exit(EXIT_SUCCESS);
}
void show_lock_info(struct flock *to_show)
{
printf("\tl_type %d, ", to_show->l_type);
printf("\tl_whence %d, ", to_show->l_whence);
printf("\tl_start %d, ", (int)to_show->l_start);
printf("\tl_len %d, ", (int)to_show->l_len);
printf("\tl_pid %d\n", to_show->l_pid);
}
lock4 程序把文件中的每5个字节分成一组,为每个组设置一个区域结构来测试锁,然后通过使用这些结构来判断对应区域是否可以被加写锁或读锁。返回信息将显示造成锁请求失败的区域字节数和从字节0开始的偏移量。因为返回结构中的l_pid 元素包含当前拥有文件锁的程序的进程标识符,所以程序先把它设置为-1,然后在 fcntl 调用返回后测试其值是否被修改过。如果该区域当前未被锁定,l_pid的值就不会被改变。
在共享锁上可以再次上共享锁,在独占锁上无法上任何类型锁,未上锁的区域可以进行解锁。
|