Linux操作系统分析实验-多线程与内核模块编程,实验一_实验一 多线程与内核模块编程-CSDN博客
一、实验目的
1、理解Linux进程地址空间、虚拟内存、物理内存的概念;
2、理解物理内存分配和回收原理。
3、利用链表实现动态内存分配。
4、了解共享内存原理,并深入理解用户与内核共享内存过程。
二、实验内容
1、利用strace 命令跟踪进程调用 exec 函数将可执行文件载入内存时,代码段,数据段,bbs 段,stack 段都通过 mmap 函数映射到内存空间的过程;
2、利用free命令查看当前系统的内存使用情况;
3、 利用链表结构,malloc函数和 free函数实现将终端输入的一系列字符串用链表的形式保存下来。然后再将这些数据组装起来,回显到输出终端。
4、 利用vmalloc()和vfree()内核函数编写内核模块实现内核和用户共享内存。
三、实验原理
1、静态内存与动态内存
按分配内存空间的方式不同,一个程序所使用的内存区域可以分为静态内存与动态内存。在程序开始运行时由系统分配的内存称为静态内存,在程序运行过程中由用户自己申请分配的内存称为动态内存。
静态内存的申请是由编译器来分配的。当程序执行时,系统就为变量分配所需的内存空间,至使用该变量的函数执行完毕返回时,自动释放所占用的内存空间。用户并不需要了解分配内存的具体细节,也不需要时刻考虑由于程序结束前未释放所占用的内存空间而带来的可用内存泄漏。同时,静态内存也是不通过指针而使用变量的唯一方法。
使用动态内存时,用户可以根据需要随时申请内存,使用完毕后手动将此内存区释放。在实际应用中非常方便。
2、使用链表进行动态内存的分配
虽然使用动态内存可以方便地使用内存,但动态内存也有局限性,就是在数据输入到程序之前必须知道数据的大小,以便申请相应的动态内存。
链表是一种动态地进行存储分配的结构。链表中的各个元素是一个结构,每个元素称为链表的一个结点。此结构中包含有一个指向此结构的指针,用于指向链表中的下一个结点。链表的最后一个结点的指针NULL,表示链表结束。
3、进程地址空间
程序一旦被执行就成为一个进程,内核就会为每个运行的进程提供了大小相同的虚拟地址空间,这使得多个进程可以同时运行而又不会互相干扰。
每个进程通过系统调用进入内核,Linux内核空间由系统内的所有进程共享。从进程的角度看,每个进程拥有4GB的虚拟地址空间。每个进程有各自的私有用户空间(0-3GB),这个空间对系统中的其他进程是不可见的。最高的1GB内核空间为所有进程以及内核所共享。
每个程序编译链接后形成的二进制映像文件有一个代码段(Text)和数据段(BSS和Data)。进程运行时须有独占的堆(Heap)和栈(Stack)空间。进程要映射的文件被映射到内存映射区(Memory Mapping Region)。
Linux 把进程的用户空间划分为若干个区,便于管理,这些区间称为虚拟内存域(简称vma )。 一个进程的用户地址空间主要由 mm_struct 结构和 vm_area_struct 结构来描述。
4、进程用户空间的创建
每个进程都有自己独立的地址空间,那么,进程的地址空间到底是什么时候创建的?实际上,当fork()系统调用在创建新进程时也为该进程创建完整的用户空间。那么这个用户空间是如何被创建的呢?实际上是通过拷贝或共享父进程的用户空间来实现的,即内核调用copy_mm()函数,为新进程建立所有页表和mm_struct结构(通常,每个进程都有自己的用户空间,但是调用clone( )函数创建内核线程时共享父进程的用户空间)。
5、虚存映射
当调用exec()系统调用开始执行一个进程时,进程的可执行映像(包括代码段、数据段,堆和栈等)必须装入到进程的用户地址空间。如果该进程用到了任何一个共享库,则共享库也必须装入到进程的用户空间。由此可看出,Linux并不将映像装入到物理内存,相反,可执行文件只是被映射到进程的用户空间中。这种将可以执行文件映像映射到进程用户空间的方法被称为“虚存映射”。
6、页故障与物理内存的分配
一个程序在装载时,执行文件的指令和数据加载进内存,但并没有真正的装入物理内存,只是通过ELF文件头部信息建立起可执行文件与虚拟地址空间的映射关系而已,真正加载过程将在发生缺页异常处理时才进行。
在访问用户态虚拟地址空间时,如果没有映射物理地址,通过请页机制发出缺页异常。缺页异常陷入内核,分配物理地址空间,并将可执行文件内容装载到该内存页中,与用户态虚拟地址建立映射关系,也就是填充页表。找到所需的内容在可执行文件中的位置,将指令寄存器设置为可执行文件入口,然后把控制权交换给进程,启动运行。进程运行时,CPU访问的是用户空间的虚地址。Linux仅把当前要使用的少量页面装入内存,需要时再通过请页机制将特定的页面调入内存。当要访问的页不在内存时,产生一个页故障并报告故障原因。
这其中涉缺页异常处理机制和物理内存的分配与回收。
当用户进程通过调用系统调用申请内存时,首先陷入内核,建立虚拟地址空间的映射,获得一块虚拟内存区VMA。当进程对这块虚存区进行访问时,如果物理内存尚未分配,那么此时发生一个缺页异常,通过get_free_pages 申请一个或多个物理页面,并将此物理内存和虚拟内存的映射关系写入页表。
四、实验步骤
要求写出实验过程和思路(用文字表述,或画流程图,或写出伪代码 都可以)。
1、利用strace 命令跟踪进程调用 exec 函数将可执行文件载入内存时,代码段,数据段,bbs 段,stack 段都通过 mmap 函数映射到内存空间的过程;
打开终端,执行 strace -e trace=mmap,execve 可执行文件路径 命令,跟踪可执行文件的加载过程。
观察输出,查看mmap系统调用的参数,了解代码段、数据段、bbs段、stack段的映射情况。
2、利用free命令查看当前系统的内存使用情况;
打开终端,执行 free 命令。
查看输出,了解系统的内存使用情况。
3、利用链表结构,malloc函数和 free函数实现将终端输入的一系列字符串用链表的形式保存下来。然后再将这些数据组装起来,回显到输出终端。
使用链表结构保存输入的字符串,每个节点存储一个字符串。
利用malloc函数动态分配内存来存储每个字符串。
利用free函数释放链表占用的内存。
将链表中的字符串组装起来,并回显到输出终端。
4、利用vmalloc()和vfree()内核函数编写内核模块实现内核和用户共享内存。
- 编写内核模块,使用vmalloc函数在内核空间分配共享内存。
- 在模块初始化函数中初始化共享内存,将一些数据存储到共享内存中。
- 通过字符设备接口实现mmap函数,将共享内存映射到用户空间。
- 编写用户空间程序,调用mmap函数获取共享内存的映射地址。
- 在用户空间程序中访问共享内存,进行读写操作。
- 在模块卸载函数中释放共享内存。
五、实验数据及源代码
(学生必须提交程序源代码,并有注释)
1.
strace -e trace=mmap,execve /home/fengshu/first(可执行文件路径)
2.
free
该命令将显示系统总内存、已使用内存和空闲内存的信息。
3.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义链表节点的结构
struct Node {
char data[100];
struct Node* next;
};
// 将新节点添加到链表的函数
void addNode(struct Node** head, const char* data) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
strncpy(newNode->data, data, sizeof(newNode->data) - 1);
newNode->next = *head;
*head = newNode;
}
// 显示链表的函数
void displayList(struct Node* head) {
struct Node* current = head;
while (current != NULL) {
printf("%s\n", current->data);
current = current->next;
}
}
// 释放链表占用的内存的函数
void freeList(struct Node* head) {
struct Node* current = head;
while (current != NULL) {
struct Node* temp = current;
current = current->next;
free(temp);
}
}
int main() {
struct Node* head = NULL;
char input[100];
// 从终端读取一系列字符串
printf("input chars(exit:end):\n");
while (1) {
scanf("%s", input);
if (strcmp(input, "exit") == 0) {
break;
}
addNode(&head, input);
}
// 显示链表
printf("\nList content:\n");
displayList(head);
// 释放链表占用的内存
freeList(head);
return 0;
}
4.
#include <linux/init.h>
#include <linux/module.h>
#include <linux/vmalloc.h>
#include <linux/fs.h>
#include <linux/mm.h>
#define MODULE_NAME "shared_memory_module"
static char* shared_memory;
static int my_open(struct inode* inode, struct file* filp) {
return 0;
}
static int my_release(struct inode* inode, struct file* filp) {
return 0;
}
static int my_mmap(struct file* filp, struct vm_area_struct* vma) {
int size = vma->vm_end - vma->vm_start;
int result;
// 将共享内存映射到进程的地址空间
result = remap_pfn_range(vma, vma->vm_start, virt_to_phys(shared_memory) >> PAGE_SHIFT, size, vma->vm_page_prot);
if (result < 0) {
printk(KERN_INFO "映射共享内存失败\n");
return -EIO;
}
return 0;
}
static struct file_operations my_fops = {
.open = my_open,
.release = my_release,
.mmap = my_mmap,
};
static int __init my_init(void) {
shared_memory = vmalloc(PAGE_SIZE); // 分配共享内存
if (!shared_memory) {
printk(KERN_INFO "分配共享内存失败\n");
return -ENOMEM;
}
// 初始化共享内存(例如,存储一些数据)
strcpy(shared_memory, "来自内核的问候!");
// 注册字符设备
if (register_chrdev(0, MODULE_NAME, &my_fops)) {
printk(KERN_INFO "注册字符设备失败\n");
vfree(shared_memory);
return -EFAULT;
}
printk(KERN_INFO "模块加载成功\n");
return 0;
}
static void __exit my_exit(void) {
unregister_chrdev(0, MODULE_NAME); // 注销字符设备
vfree(shared_memory); // 释放共享内存
printk(KERN_INFO "模块卸载\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
六、实验结果分析(截屏的实验结果,与实验结果对应的实验分析)
1)实验结果与实验程序、实验步骤、实验原理的对应分析;
1、利用strace 命令跟踪进程调用 exec 函数将可执行文件载入内存时,代码段,数据段,bbs 段,stack 段都通过 mmap 函数映射到内存空间的过程;
2、利用free命令查看当前系统的内存使用情况;
3、利用链表结构,malloc函数和 free函数实现将终端输入的一系列字符串用链表的形式保存下来。然后再将这些数据组装起来,回显到输出终端。
4、利用vmalloc()和vfree()内核函数编写内核模块实现内核和用户共享内存。
2)实验过程中的问题及原因和解决办法。
在使用共享内存之前,必须确保正确初始化。否则,可能会导致未定义的行为或错误的结果。解决方法: 在共享内存分配后,执行必要的初始化步骤,确保共享内存处于有效状态。
如果多个进程或线程同时访问共享内存,可能导致竞态条件或数据不一致性。解决方法: 使用同步机制(如信号量、互斥锁等)来确保对共享内存的安全访问。
七、实验思考题
1、进程的地址空间(mm_struct结构和vm_area_strcut结构)到底是什么时候被映射的?
进程的地址空间在进程创建时被初始化。在Linux中,mm_struct结构表示进程的内存描述符,而vm_area_struct结构则表示虚拟内存区域。地址空间的映射发生在以下几个时刻:
- 进程创建: 当一个新的进程被创建时,它会继承父进程的地址空间,这时地址空间会被映射到新的进程。
- 执行可执行文件: 当进程调用exec函数加载可执行文件时,新的代码段、数据段等会被映射到地址空间。
- 动态内存分配: 当进程使用malloc等动态内存分配函数时,内存会被动态映射到地址空间。
- 文件映射: 当进程使用mmap函数将文件映射到内存时,文件的内容会被映射到地址空间。
2、什么是内存泄漏?为什么会发生内存泄漏?
内存泄漏是指程序在运行过程中动态分配的内存空间没有被正确释放,导致系统中的可用内存逐渐减少。
为什么会发生内存泄露:
- 未释放动态分配的内存: 当程序动态分配内存(使用malloc或new等函数),但在程序运行结束前没有正确释放这些内存时,就会导致内存泄漏。
- 丢失对动态分配内存的引用: 当程序丢失了对动态分配内存的指针,无法再进行释放操作时,也会导致内存泄漏。
3、什么是野指针?为什么会出现野指针?
野指针是指指向已经被释放或者无效的内存地址的指针。
出现野指针的原因主要有两种:
- 指针未被初始化: 当一个指针被声明但未被初始化时,它的值是不确定的,可能指向任意内存地址,这种情况下使用该指针就会导致野指针问题。
- 指针指向的内存已经被释放: 当程序使用free或delete等函数释放了动态分配的内存后,如果继续使用指向该内存的指针,就会引发野指针问题。
4、通过 mmap()进行内存映射,多进程是否安全?
通过mmap()进行内存映射时,多进程是可以安全地共享内存的。mmap()函数创建的映射区域可以被多个进程访问,实现了进程间的共享内存。但是需要注意在多进程共享内存时要确保正确同步数据,以避免竞争条件和数据一致性问题。
5、用户空间(进程)是否有高端内存概念?
没有高端内存的概念
6、64 位内核中有高端内存吗?
在64位系统中,通常不再限制用户空间和内核空间的大小,因此不存在32位系统中的"高端内存"概念。
7、在32 位和 64 位系统上,用户进程能访问多少物理内存?内核代码能访问多少物理内存?
在32位系统上,用户进程一般能够访问的物理内存受限于系统的地址空间限制,通常为4GB。内核代码能够访问的物理内存也受限于1GB的内核空间。
在64位系统上,用户进程能够访问的物理内存理论上可以非常大,取决于系统硬件和操作系统的支持。64位系统的地址空间范围极大,一般可以支持数十TB的物理内存。内核代码也能够访问非常大的物理内存。
8、在物理内存为 1G 的计算机中,能否调用 malloc(1.6G )?为什么?
在物理内存为1G的计算机中,调用malloc(1.6G)可能会导致分配失败。这是因为malloc在分配内存时需要找到足够大的连续空闲内存块,如果系统中没有足够大的连续空闲内存块,分配就会失败。物理内存大小限制了malloc函数能够分配的最大内存块的大小