Linux内存源码分析-内存池源码分析

2023-02-20 00:00:00 函数 对象 内存 主机 握手

  内存池是用于预先申请一些内存用于备用,当系统内存不足无法从伙伴系统和slab中获取内存时,会从内存池中获取预留的那些内存。内存池与特殊slab一样,需要使用的设备需要自己创建内存池,而不是系统会自动生成。书上形容得好,内存比作新鲜食物,内存池比作罐头食物,人比作拥有此内存池的模块,当无法吃到新鲜食物时,就需要打开罐头吃罐头食物。

  一般情况下,内存池建立在slab之上,也就是说池子里存放的是slab对象,当某个模块创建一个属于自己的内存池时,创建之前,需要设计好一个分配函数和一个释放函数,创建函数用于创建内存池时自动申请对应的slab对象,而释放函数则是用于将slab对象释放回到slab/slub中。当然经过看代码,感觉内存池中也可以存放页,这个可以写个模块测试一下。

  具体先看看内存池主要的数据结构:

/* 内存池,用于拥有该内存池的"拥有者"进行内存储备,只有在常规情况下分配不到内存的时候才会使用自己的内存池 */

typedef struct mempool_s {

spinlock_t lock;

/* 大元素个数,也是初始个数,当内存池被创建时,会调用alloc函数申请此变量相应数量的slab放到elements指向的指针数组中 */

int min_nr; /* nr of elements at *elements */

/* 当前元素个数 */

int curr_nr; /* Current nr of elements at *elements */

/* 指向一个数组,在mempool_create中会分配内存,数组中保存指向元素指针 */

void **elements;


/* 内存池的拥有者的私有数据结构,当元素是slab中的对象时,这里保存的是slab缓存描述符 */

void *pool_data;

/* 当元素是slab中的对象时,会使用方法mempool_alloc_slab()和mempool_free_slab() */

/* 分配一个元素的方法 */

mempool_alloc_t *alloc;

/* 释放一个元素的方法 */

mempool_free_t *free;

/* 当内存池为空时使用的等待队列,当内存池中空闲内存对象为空时,获取函数会将当前进程阻塞,直到超时或者有空闲内存对象时才会唤醒 */

wait_queue_head_t wait;

} mempool_t;

  内核里使用mempool_create()创建一个内存池,使用mempool_destroy()销毁一个内存池,使用mempool_alloc()申请内存和mempool_free()是否内存。一切信息都在代码当中,我们直接看代码就清楚知道内存池是怎么实现的了。

  首先我们先看mempool_create(),mempool_create()流程很简单,它主要做的就是分配一个mempool_t结构体,然后根据参数初始化此结构体,后调用传入的alloc()函数min_nr次,把申请到的内存全部存放到elements中,如下:

mempool_t *mempool_create_node(int min_nr, mempool_alloc_t *alloc_fn,

mempool_free_t *free_fn, void *pool_data,

gfp_t gfp_mask, int node_id)

{

mempool_t *pool;

/* 分配一个内存池结构体 */

pool = kzalloc_node(sizeof(*pool), gfp_mask, node_id);

if (!pool)

return NULL;

/* 分配一个长度为min_nr的数组用于存放申请后对象的指针 */

pool->elements = kmalloc_node(min_nr * sizeof(void *),

gfp_mask, node_id);

if (!pool->elements) {

kfree(pool);

return NULL;

}

/* 初始化锁 */

spin_lock_init(&pool->lock);

pool->min_nr = min_nr;

/* 私有成员 */

pool->pool_data = pool_data;

/* 初始化等待队列 */

init_waitqueue_head(&pool->wait);

pool->alloc = alloc_fn;

pool->free = free_fn;


/*

* First pre-allocate the guaranteed number of buffers.

*/

/* pool->curr_nr初始为0,因为pool使用kzalloc_node分配的,会清0 */

while (pool->curr_nr < pool->min_nr) {

void *element;


/* 调用pool->alloc函数min_nr次 */

element = pool->alloc(gfp_mask, pool->pool_data);

/* 如果申请不到element,则直接销毁此内存池 */

if (unlikely(!element)) {

mempool_destroy(pool);

return NULL;

}

/* 添加到elements指针数组中 */

add_element(pool, element);

}

/* 返回内存池结构体 */

return pool;

}

  再看看mempool_destroy(),此函数也很简单,直接将elements存放的内存依个释放掉,然后将该释放的elements指针数组和mempool_t结构都释放掉

/* 销毁一个内存池 */

void mempool_destroy(mempool_t *pool)

{

while (pool->curr_nr) {

/* 销毁elements数组中的所有对象 */

/* element = pool->elements[--pool->curr_nr] */

void *element = remove_element(pool);

pool->free(element, pool->pool_data);

}

/* 销毁elements指针数组 */

kfree(pool->elements);

/* 销毁内存池结构体 */

kfree(pool);

}

   现在我们看mempool_alloc()函数,当模块从此内存池中获取内存对象时,会调用此函数,此函数优先从伙伴系统或slab缓冲区获取需要的内存对象,当内存不足导致无法获取内存对象时,才会从内存池elements数组中获取,如果elements也没有空闲的内存对象,根据传入的分配标识进行相应的处理:

/* 内存池分配对象 */

void * mempool_alloc(mempool_t *pool, gfp_t gfp_mask)

{

void *element;

unsigned long flags;

wait_queue_t wait;

gfp_t gfp_temp;


VM_WARN_ON_ONCE(gfp_mask & __GFP_ZERO);

/* 如果有__GFP_WAIT标志,则会先阻塞,切换进程 */

might_sleep_if(gfp_mask & __GFP_WAIT);


/* 不使用预留内存 */

gfp_mask |= __GFP_NOMEMALLOC; /* don't allocate emergency reserves */

/* 分配页时如果失败则返回,不进行重试 */

gfp_mask |= __GFP_NORETRY; /* don't loop in __alloc_pages */

/* 分配失败不提供警告 */

gfp_mask |= __GFP_NOWARN; /* failures are OK */


/* gfp_temp等于gfp_mask去除__GFP_WAIT和__GFP_IO的其他标志 */

gfp_temp = gfp_mask & ~(__GFP_WAIT|__GFP_IO);


repeat_alloc:


/* 使用内存池中的alloc函数进行分配对象,实际上就是从伙伴系统或者slab缓冲区获取内存对象 */

element = pool->alloc(gfp_temp, pool->pool_data);

/* 在内存富足的情况下,一般是能够获取到内存的 */

if (likely(element != NULL))

return element;


/* 在内存不足的情况,造成从伙伴系统或slab缓冲区获取内存失败,则会执行到这 */

/* 给内存池上锁,获取后此段临界区禁止中断和抢占 */

spin_lock_irqsave(&pool->lock, flags);

/* 如果当前内存池中有空闲数量,就是初始化时获取的内存数量保存在curr_nr中 */

if (likely(pool->curr_nr)) {

/* 从内存池中获取内存对象 */

element = remove_element(pool);

/* 解锁 */

spin_unlock_irqrestore(&pool->lock, flags);


/* 写内存屏障,保证之前的写操作已经完成 */

smp_wmb();

/* 用于debug */

kmemleak_update_trace(element);

return element;

}


/* 这里是内存池中也没有空闲内存对象的时候进行的操作 */

/* gfp_temp != gfp_mask说明传入的gfp_mask允许阻塞等待,但是之前已经阻塞等待过了,所以这里立即重新获取一次 */

if (gfp_temp != gfp_mask) {

spin_unlock_irqrestore(&pool->lock, flags);

gfp_temp = gfp_mask;

goto repeat_alloc;

}


/* 传入的参数gfp_mask不允许阻塞等待,分配不到内存则直接退出 */

if (!(gfp_mask & __GFP_WAIT)) {

spin_unlock_irqrestore(&pool->lock, flags);

return NULL;

}


init_wait(&wait);

/* 加入到内存池的等待队列中,并把当前进程的状态设置为只有wake_up信号才能唤醒的状态 ,也就是当内存池中有空闲对象时,会主动唤醒等待队列中的个进程,或者等待超时时,定时器自动唤醒 */

prepare_to_wait(&pool->wait, &wait, TASK_UNINTERRUPTIBLE);


spin_unlock_irqrestore(&pool->lock, flags);


/* 阻塞等待5秒 */

io_schedule_timeout(5*HZ);


/* 从内存池的等待队列删除此进程 */

finish_wait(&pool->wait, &wait);

/* 跳转到repeat_alloc,重新尝试获取内存对象 */

goto repeat_alloc;

}

EXPORT_SYMBOL(mempool_alloc);

  后我们看看mempool_free()函数,此函数用于将空闲内存对象释放到内存池中,当内存池中空闲对象不足时,优先将空闲内存对象放到elements数组中,否则直接返回到伙伴系统或slab缓冲区中。

/* 内存池释放内存对象操作 */

void mempool_free(void *element, mempool_t *pool)

{

unsigned long flags;


/* 传入的对象为空,则直接退出 */

if (unlikely(element == NULL))

return;


/* 读内存屏障 */

smp_rmb();


/* 如果当前内存池中空闲的内存对象少于内存池中应当保存的内存对象的数量时,优先把释放的对象加入到内存池空闲数组中 */

if (unlikely(pool->curr_nr < pool->min_nr)) {

spin_lock_irqsave(&pool->lock, flags);

if (likely(pool->curr_nr < pool->min_nr)) {

/* 加入到pool->elements[pool->curr_nr++]中 */

add_element(pool, element);

spin_unlock_irqrestore(&pool->lock, flags);

/* 唤醒等待队列中的个进程 */

wake_up(&pool->wait);

return;

}

spin_unlock_irqrestore(&pool->lock, flags);

}

/* 直接调用释放函数 */

pool->free(element, pool->pool_data);

}

EXPORT_SYMBOL(mempool_free);

  或许看完这些还是不太清楚怎么使用内存池,毕竟alloc和free函数需要我们自己去设计,如果内存池是使用slab缓冲区进行内存分配时,可将slab缓冲区描述符写入到mempool_t中的pool_data中,alloc和free函数可以直接指定mempool_alloc_slab()和mempool_free_slab(),如下:

/* 创建一个slab缓冲区 */

drbd_request_cache = kmem_cache_create(

"drbd_req", sizeof(struct drbd_request), 0, 0, NULL);

if (drbd_request_cache == NULL)

goto Enomem;


/* 创建一个内存池,私有成员设置为drbd_request_cache这个slab缓冲区,alloc和free函数设置为mempool_alloc_slab()和mempool_free_slab() */

drbd_request_mempool = mempool_create(number,mempool_alloc_slab,mempool_free_slab,drbd_request_cache);

if (drbd_request_mempool == NULL)

goto Enomem;



/* 若内存池从slab缓冲区中获取内存对象,则内核提供的alloc函数 */

void *mempool_alloc_slab(gfp_t gfp_mask, void *pool_data)

{

struct kmem_cache *mem = pool_data;

return kmem_cache_alloc(mem, gfp_mask);

}

EXPORT_SYMBOL(mempool_alloc_slab);


/* 若内存池从slab缓冲区中获取内存对象,则内核提供的free函数 */

void mempool_free_slab(void *element, void *pool_data)

{

struct kmem_cache *mem = pool_data;

kmem_cache_free(mem, element);

}

EXPORT_SYMBOL(mempool_free_slab);


TCP协议为什么会采用三次握手,若采用二次握手可以吗?

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接(连接导向)的、可靠的、基于IP的传输层协议,采用三次握手确认建立一个连接。

TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。

在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。

次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器 进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED状态,完成三次握手。

通过这样的三次握手,客户端与服务端建立起可靠的双工的连接,开始传送数据。

三次握手的主要目的是保证连接是双工的,可靠更多的是通过重传机制来保证的。
(1)TCP 的三次握手过程:主机A向B发送连接请求;主机B对收到的主机A的报文段进行确认;主A再次对主机B的确认进行确认。
(2)采用三次握手是为了防止失效的连接请求报文段突然又传送到主机B,因而产生错误。失效的连接请求报文段是指:主机A发出的连接请求没有收到主机B的确认,于是经过一段时间后,主机A又重新向主机B发送连接请求,且建立成功,顺序完成数据传输。考虑这样一种特殊情况,主机A次发送的连接请求并没有丢失,而是因为网络节点导致延迟达到主机B,主机B以为是主机A又发起的新连接,于是主机B同意连接,并向主机A发回确认,但是此时主机A根本不会理会,主机B就一直在等待主机A发送数据,导致主机B的资源浪费。
为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。

你们都是有经验

相关文章