linux0.11内核源码剖析第一篇:memory.c
July 二零一一年一月六日
-----------------------------------------
博主声明:
1.本系列非linux系统教程,仅仅是针对linux0.11内核源码,所做的剖析,注释。
2.本系列参考:深入理解linux内核、linux内核完全注释,linux内核源代码情景分析
3.本文代码:linux0.11内核源码
4.本文图片:手工画成、linux内核完全注释
以下所写的所有任何文字,皆是、做纯粹性研究、学习之用。有误之处,望不吝指正。
其中大部分注释来自inux内核源码注释一书中,我对其做了整理、添加和修正。
文章所列出的源码,并不是某个单独.c或.s文件的全部源码,略去了部分宏、头文件之类。
同时,我也尽量不加关系不大,而读者自能明了其中内容的注释,
如此,实是为了在易懂之余兼顾简洁。
本系列文章排版约定:
某一个.c程序,包含着许多具体的函数,用序号粗体字1、2、3...表示着各个函数。
列出某一函数的具体源码之前,一般会有一段说明文字,对此函数进行描述与介绍。
而后,可能还会在“//”后,列出部分注释。
具体函数分析中,“//.....”为对当前一行,所添加的注释。
------------------------------------------------------
ok,开始。
我们知道,linux0.11内核源码如下,
第一篇 内存管理(mm)
所谓侯捷先生所说,源码之前,了无秘密,咱们抛开所有繁杂的文字叙述,直接从内核源码下手,
咱们一起拿着手术刀把linux0.11内核源码,层层剖开,一窥其中的能窥到的部分秘密。
当然,我更多的只是,提供了这样一份资料给你,更多的东西,还得等你自己去挖掘。
另外,如果分析源码的过程中,你对某个或多个细节不甚明白,不要紧,尽管继续看下去,
答案就在下文,你自会看到且弄懂的。
咱们,先从内存管理(mm)那一章,开始剖析。
ok,下面,请直接看,memory.c程序:
//直接给注释,参考linux内核完全注释。
本文,共计14个函数,如下我画的图:
linux/mm/memory.c :
/*
* linux/mm/memory.c
* (C) 1991 Linus Torvalds
* CopyRight@http://blog.****.net/v_JULY_v
*/
#include <signal.h>
//信号头文件。定义信号符号常量,信号结构以及信号操作函数原型。
#include <asm/system.h>
//系统头文件。定义了设置或修改描述符/中断门等的嵌入式汇编宏。
#include <linux/sched.h>
//调度程序头文件,定义了任务结构task_struct、初始任务0 的数据,
#include <linux/head.h>
//head 头文件,定义了段描述符的简单结构,和几个选择符常量。
#include <linux/kernel.h>
//内核头文件。含有一些内核常用函数的原形定义。
volatile void do_exit (long code);
//进程退出处理函数,在kernel/exit.c中(日后剖析)。
//显示内存已用完出错信息,并退出。
static inline volatile void oom (void)
{
printk ("out of memory\n\r");
do_exit (SIGSEGV); // do_exit()应该使用退出代码,这里用了信号值SIGSEGV
(11)
} // 相同值的出错码含义是“资源暂时不可用”,正好同义。
1、get_free_page函数
--------------------------------------
获取首个(实际上是最后1 个:-)空闲页面,并标记为已使用。
//如果没有空闲页面,就返回0。
unsigned long get_free_page (void)
{
register unsigned long __res asm ("ax");
__asm__ ("std ; repne ; scasb\n\t" // 方向位置位,将al(0)与对应每个页面的(di)内容
比较,
"jne 1f\n\t" // 如果没有等于0 的字节,则跳转结束(返回0)。
"movb $1,1(%%edi)\n\t" // 将对应页面的内存映像位置1。
"sall $12,%%ecx\n\t" // 页面数*4K = 相对页面起始地址。
"addl %2,%%ecx\n\t" // 再加上低端内存地址,即获得页面实际物理起始地址。
"movl %%ecx,%%edx\n\t" // 将页面实际起始地址->edx 寄存器。
"movl $1024,%%ecx\n\t" // 寄存器ecx 置计数值1024。
"leal 4092(%%edx),%%edi\n\t" // 将4092+edx 的位置->edi(该页面的末端)。
"rep ; stosl\n\t" // 将edi 所指内存清零(反方向,也即将该页面清零)。
"movl %%edx,%%eax\n" // 将页面起始地址->eax(返回值)。
"1:": "=a" (__res): "" (0), "i" (LOW_MEM), "c" (PAGING_PAGES), "D" (mem_map +
PAGING_PAGES - 1):"di", "cx",
"dx");
return __res; // 返回空闲页面地址(如果无空闲也则返回0)。
}
2、free_page函数
------------------------------------
释放物理地址\'addr\'开始的一页内存。用于函数\'free_page_tables()\'。
释放物理地址addr 开始的一页面内存。
1MB 以下的内存空间用于内核程序和缓冲,不作为分配页面的内存空间。
void free_page (unsigned long addr)
{
if (addr < LOW_MEM)
return; // 如果物理地址addr 小于内存低端(1MB),则返
回。
if (addr >= HIGH_MEMORY) // 如果物理地址addr>=内存最高端,则显示出错信息。
panic ("trying to free nonexistent page");
addr -= LOW_MEM; // 物理地址减去低端内存位置,再除以4KB,得页面号。
addr >>= 12;
if (mem_map[addr]--)
return; // 如果对应内存页面映射字节不等于0,则减1 返回。
mem_map[addr] = 0; // 否则置对应页面映射字节为0,并显示出错信息,死机。
panic ("trying to free free page");
}
3、free_page_tables函数
------------------------------------------
下面函数释放页表连续的内存块,\'exit()\'需要该函数。
与copy_page_tables()类似,该函数仅处理4Mb 的内存块。
int free_page_tables (unsigned long from, unsigned long size)
{
unsigned long *pg_table;
unsigned long *dir, nr;
if (from & 0x3fffff) // 要释放内存块的地址需以4M 为边界。
panic ("free_page_tables called with wrong alignment");
if (!from) // 出错,试图释放内核和缓冲所占空间。
panic ("Trying to free up swapper memory space");
// 计算所占页目录项数(4M 的进位整数倍),也即所占页表数。
size = (size + 0x3fffff) >> 22;
dir = (unsigned long *) ((from >> 20) & 0xffc); /* _pg_dir = 0 */
for (; size-- > 0; dir++)
{ // size 现在是需要被释放内存的目录项数。
if (!(1 & *dir)) // 如果该目录项无效(P 位=0),则继续。
continue; // 目录项的位0(P 位)表示对应页表是否存在。
pg_table = (unsigned long *) (0xfffff000 & *dir); // 取目录项中页表地址
。
for (nr = 0; nr < 1024; nr++)
{ // 每个页表有1024 个页项。
if (1 & *pg_table) // 若该页表项有效(P 位=1),则释放对应内存页。
free_page (0xfffff000 & *pg_table);
*pg_table = 0; // 该页表项内容清零。
pg_table++; // 指向页表中下一项。
}
free_page (0xfffff000 & *dir); // 释放该页表所占内存页面。但由于页表在
// 物理地址1M 以内,所以这句什么都不做。
*dir = 0; // 对相应页表的目录项清零。
}
invalidate (); // 刷新页变换高速缓冲。
return 0;
}
4、copy_page_tables函数
----------------------------------------
目的则是为了将当前源目录项from_page_table,复制到目的页表to_page_table中。
int copy_page_tables (unsigned long from, unsigned long to, long size)
{
unsigned long *from_page_table;
unsigned long *to_page_table;
unsigned long this_page;
unsigned long *from_dir, *to_dir;
unsigned long nr; //复制时,指定nr个数目内存页面待复制
// 源地址和目的地址都需要是在4Mb 的内存边界地址上。否则出错,死机。
if ((from & 0x3fffff) || (to & 0x3fffff))
panic ("copy_page_tables called with wrong alignment");
// 取得源地址和目的地址的目录项(from_dir 和to_dir)。
from_dir = (unsigned long *) ((from >> 20) & 0xffc); /* _pg_dir = 0 */
to_dir = (unsigned long *) ((to >> 20) & 0xffc);
// 计算要复制的内存块占用的页表数(也即目录项数)。
size = ((unsigned) (size + 0x3fffff)) >> 22;
// 下面开始对每个占用的页表依次进行复制操作。
for (; size-- > 0; from_dir++, to_dir++)
{
// 如果目的目录项指定的页表已经存在(P=1),则出错,死机。
if (1 & *to_dir)
panic ("copy_page_tables: already exist");
// 如果此源目录项未被使用,则不用复制对应页表,跳过。
if (!(1 & *from_dir))
continue;
// 取当前源目录项中页表的地址->from_page_table。
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
// 为目的页表取一页空闲内存,如果返回是0,则说明没有申请到空闲内存页面。返回值
=-1,退出。
if (!(to_page_table = (unsigned long *) get_free_page ()))
return -1; /* Out of memory, see freeing */
// 设置目的目录项信息,7 是标志信息,表示(Usr, R/W, Present)。
*to_dir = ((unsigned long) to_page_table) | 7;
// 针对当前处理的页表,设置需复制的页面数。
//如果是在内核空间,则仅需复制头160 页,否则需要复制1 个页表中的所有1024 页面
。
nr = (from == 0) ? 0xA0 : 1024;
// 对于当前页表,开始复制指定数目nr 个内存页面。
for (; nr-- > 0; from_page_table++, to_page_table++)
{
this_page = *from_page_table; // 取源页表项内容。
if (!(1 & this_page)) // 如果当前源页面没有使用,则不用复制。
continue;
// 复位页表项中R/W 标志(置0)。
//(如果U/S 位是0,则R/W 就没有作用。如果U/S 是1,而R/W 是0,
//那么运行在用户层的代码就只能读页面。如果U/S 和R/W 都置位,则就有写的
权限。)
this_page &= ~2;
*to_page_table = this_page; // 将该页表项复制到目的页表中。
// 如果该页表项所指页面的地址在1M 以上,则需要设置内存页面映射数组
mem_map[],
//于是计算页面号,并以它为索引在页面映射数组相应项中增加引用次数。
if (this_page > LOW_MEM)
{
// 下面这句的含义是令源页表项所指内存页也为只读。因为现在开始
有两个进程共用内存区了。
// 若其中一个内存需要进行写操作,则可以通过页异常的写保护处理
,
//为执行写操作的进程分配一页新的空闲页面,也即进行写时复制的操
作。
*from_page_table = this_page; // 令源页表项也只读。
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate (); // 刷新页变换高速缓冲。
return 0;
}
5、put_page函数
--------------------------------------------
下面函数将一内存页面放置在指定地址处。它返回页面的物理地址,如果
内存不够(在访问页表或页面时),则返回0。
//把一物理内存页面映射到指定的线性地址处。
//主要工作是在页目录和页表中设置指定页面的信息。若成功则返回页面地址。
unsigned long put_page (unsigned long page, unsigned long address)
{
unsigned long tmp, *page_table;
/* NOTE !!! This uses the fact that _pg_dir=0 */
/* 注意!!!这里使用了页目录基址_pg_dir=0 的条件 */
// 如果申请的页面位置低于LOW_MEM(1Mb)或超出系统实际含有内存高端HIGH_MEMORY,则发出警告
。
if (page < LOW_MEM || page >= HIGH_MEMORY)
printk ("Trying to put page %p at %p\n", page, address);
// 如果申请的页面在内存页面映射字节图中没有置位,则显示警告信息。
if (mem_map[(page - LOW_MEM) >> 12] != 1)
printk ("mem_map disagrees with %p at %p\n", page, address);
// 计算指定地址在页目录表中对应的目录项指针。
page_table = (unsigned long *) ((address >> 20) & 0xffc);
// 如果该目录项有效(P=1)(也即指定的页表在内存中),则从中取得指定页表的地址??page_table
。
if ((*page_table) & 1)
page_table = (unsigned long *) (0xfffff000 & *page_table);
else
{
// 否则,申请空闲页面给页表使用,并在对应目录项中置相应标志7(User, U/S, R/W)
。然后将
// 该页表的地址??page_table。
if (!(tmp = get_free_page ()))
return 0;
*page_table = tmp | 7;
page_table = (unsigned long *) tmp;
}
// 在页表中设置指定地址的物理内存页面的页表项内容。每个页表共可有1024 项(0x3ff)。
page_table[(address >> 12) & 0x3ff] = page | 7;
/* no need for invalidate */
/* 不需要刷新页变换高速缓冲 */
return page; // 返回页面地址。
}
6、un_wp_page函数
--------------------------------------------
取消写保护页面函数。用于页异常中断过程中写保护异常的处理(写时复制)。
// 输入参数为页表项指针。
// [ un_wp_page 意思是取消页面的写保护:Un-Write Protected。]
void un_wp_page (unsigned long *table_entry)
{
unsigned long old_page, new_page;
old_page = 0xfffff000 & *table_entry; // 取原页面对应的目录项号。
// 如果原页面地址大于内存低端LOW_MEM(1Mb),并且其在页面映射字节图数组中值为1(表示仅
// 被引用1 次,页面没有被共享),则在该页面的页表项中置R/W 标志(可写),并刷新页变换
// 高速缓冲,然后返回。
if (old_page >= LOW_MEM && mem_map[MAP_NR (old_page)] == 1)
{
*table_entry |= 2;
invalidate ();
return;
}
// 否则,在主内存区内申请一页空闲页面。
if (!(new_page = get_free_page ()))
oom (); // Out of Memory。内存不够处理。
// 如果原页面大于内存低端(则意味着mem_map[]>1,页面是共享的),则将原页面的页面映射
// 数组值递减1。然后将指定页表项内容更新为新页面的地址,并置可读写等标志(U/S, R/W, P)
。
// 刷新页变换高速缓冲。最后将原页面内容复制到新页面。
if (old_page >= LOW_MEM)
mem_map[MAP_NR (old_page)]--;
*table_entry = new_page | 7;
invalidate ();
copy_page (old_page, new_page);
}
7、do_wp_page 函数
----------------------------------------
当用户试图往一个共享页面上写时,该函数处理已存在的内存页面,(写时复制)
它是通过将页面复制到一个新地址上并递减原页面的共享页面计数值实现的。
如果它在代码空间,我们就以段错误信息退出。
void do_wp_page (unsigned long error_code, unsigned long address)
{
#if 0
if (CODE_SPACE (address)) // 如果地址位于代码空间,则终止执行程序。
do_exit (SIGSEGV);
#endif
un_wp_page ((unsigned long *)
(((address >> 10) & 0xffc) + (0xfffff000 &
*((unsigned long
*) ((address >> 20) &
0xffc)))));
}
8、write_verify函数
-----------------------------------------------
写页面验证。
// 若页面不可写,则复制页面。
void write_verify (unsigned long address)
{
unsigned long page;
// 判断指定地址所对应页目录项的页表是否存在(P),若不存在(P=0)则返回。
if (!((page = *((unsigned long *) ((address >> 20) & 0xffc))) & 1))
return;
// 取页表的地址,加上指定地址的页面在页表中的页表项偏移值,得对应物理页面的页表项指针
。
page &= 0xfffff000;
page += ((address >> 10) & 0xffc);
// 如果该页面不可写(标志R/W 没有置位),则执行共享检验和复制页面操作(写时复制)。
if ((3 & *(unsigned long *) page) == 1) /* non-writeable, present */
un_wp_page ((unsigned long *) page);
return;
}
9、get_empty_page函数
----------------------------------------
取得一页空闲内存并映射到指定线性地址处。
与get_free_page()不同。get_free_page()仅是申请取得了主内存区的一页物理内存。
而该函数不仅是获取到一页物理内存页面,还进一步调用put_page(),将物理页面映射到指定的线性地址处
。
void get_empty_page (unsigned long address)
{
unsigned long tmp;
// 若不能取得一空闲页面,或者不能将页面放置到指定地址处,则显示内存不够的信息。
if (!(tmp = get_free_page ()) || !put_page (tmp, address))
{
free_page (tmp); /* 0 is ok - ignored */
oom ();
}
}
10、try_to_share 函数
-----------------------------------------
try_to_share()在任务"p"中检查位于地址"address"处的页面,看页面是否存在,是否干净。
如果是干净的话,就与当前任务共享。
注意!这里我们已假定p !=当前任务,并且它们共享同一个执行程序。
//// 尝试对进程指定地址处的页面进行共享操作。
// 同时还验证指定的地址处是否已经申请了页面,若是则出错,死机。
// 返回1-成功,0-失败。
static int try_to_share (unsigned long address, struct task_struct *p)
{
unsigned long from;
unsigned long to;
unsigned long from_page;
unsigned long to_page;
unsigned long phys_addr;
// 求指定内存地址的页目录项。
from_page = to_page = ((address >> 20) & 0xffc);
// 计算进程p 的代码起始地址所对应的页目录项。
from_page += ((p->start_code >> 20) & 0xffc);
// 计算当前进程中代码起始地址所对应的页目录项。
to_page += ((current->start_code >> 20) & 0xffc);
/* 在from 处是否存在页目录?*/
// *** 对p 进程页面进行操作。
// 取页目录项内容。如果该目录项无效(P=0),则返回。否则取该目录项对应页表地址->from。
from = *(unsigned long *) from_page;
if (!(from & 1))
return 0;
from &= 0xfffff000;
// 计算地址对应的页表项指针值,并取出该页表项内容??phys_addr。
from_page = from + ((address >> 10) & 0xffc);
phys_addr = *(unsigned long *) from_page;
/* 页面干净并且存在吗?*/
// 0x41 对应页表项中的Dirty 和Present 标志。如果页面不干净或无效则返回。
if ((phys_addr & 0x41) != 0x01)
return 0;
// 取页面的地址->phys_addr。如果该页面地址不存在或小于内存低端(1M)也返回退出。
phys_addr &= 0xfffff000;
if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
return 0;
// *** 对当前进程页面进行操作。
// 取页目录项内容->to。如果该目录项无效(P=0),则取空闲页面,并更新to_page 所指的目录项
。
to = *(unsigned long *) to_page;
if (!(to & 1))
if (to = get_free_page ())
*(unsigned long *) to_page = to | 7;
else
oom ();
// 取对应页表地址??to,页表项地址??to_page。如果对应的页面已经存在,则出错,死
机。
to &= 0xfffff000;
to_page = to + ((address >> 10) & 0xffc);
if (1 & *(unsigned long *) to_page)
panic ("try_to_share: to_page already exists");
/* 对它们进行共享处理:写保护 */
// 对p 进程中页面置写保护标志(置R/W=0 只读)。并且当前进程中的对应页表项指向它
。
*(unsigned long *) from_page &= ~2;
*(unsigned long *) to_page = *(unsigned long *) from_page;
// 刷新页变换高速缓冲。
invalidate ();
// 计算所操作页面的页面号,并将对应页面映射数组项中的引用递增1。
phys_addr -= LOW_MEM;
phys_addr >>= 12;
mem_map[phys_addr]++;
return 1;
}
11、share_page函数
-----------------------------------
share_page()试图找到一个进程,它可以与当前进程共享页面。参数address 是
当前数据空间中期望共享的某页面地址。
首先我们通过检测executable->i_count 来查证是否可行。
如果有其它任务已共享该inode,则它应该大于1。
//// 共享页面。在缺页处理时看看能否共享页面。
// 返回1 - 成功,0 - 失败。
static int share_page (unsigned long address)
{
struct task_struct **p;
// 如果是不可执行的,则返回。excutable 是执行进程的内存i 节点结构。
if (!current->executable)
return 0;
// 如果只能单独执行(executable->i_count=1),也退出。
if (current->executable->i_count < 2)
return 0;
// 搜索任务数组中所有任务。寻找与当前进程可共享页面的进程,并尝试对指定地址的页面进行
共享。
for (p = &LAST_TASK; p > &FIRST_TASK; --p)
{
if (!*p) // 如果该任务项空闲,则继续寻找。
continue;
if (current == *p) // 如果就是当前任务,也继续寻找。
continue;
if ((*p)->executable != current->executable) // 如果executable 不等,也继续
。
continue;
if (try_to_share (address, *p)) // 尝试共享页面。
return 1;
}
return 0;
}
12、do_no_page函数
-----------------------------------
页异常中断处理调用的函数。处理缺页异常情况。在page.s 程序中被调用。
参数error_code 是由CPU 自动产生,address 是页面线性地址。
void do_no_page (unsigned long error_code, unsigned long address)
{
int nr[4];
unsigned long tmp;
unsigned long page;
int block, i;
address &= 0xfffff000; // 页面地址。
// 首先算出指定线性地址在进程空间中相对于进程基址的偏移长度值。
tmp = address - current->start_code;
// 若当前进程的executable 空,或者指定地址超出代码+数据长度,则申请一页物理内存,并映
射
// 影射到指定的线性地址处。executable 是进程的i 节点结构。该值为0,表明进程刚开始设置
,
// 需要内存;而指定的线性地址超出代码加数据长度,表明进程在申请新的内存空间,也需要给
予。
// 因此就直接调用get_empty_page()函数,申请一页物理内存并映射到指定线性地址处即可。
// start_code 是进程代码段地址,end_data 是代码加数据长度。对于linux 内核,它的代码段
和
// 数据段是起始基址是相同的。
if (!current->executable || tmp >= current->end_data)
{
get_empty_page (address);
return;
}
// 如果尝试共享页面成功,则退出。
if (share_page (tmp))
return;
// 取空闲页面,如果内存不够了,则显示内存不够,终止进程。
if (!(page = get_free_page ()))
oom ();
/* remember that 1 block is used for header */
/* 记住,(程序)头要使用1 个数据块 */
// 首先计算缺页所在的数据块项。BLOCK_SIZE = 1024 字节,因此一页内存需要4 个数据块。
block = 1 + tmp / BLOCK_SIZE;
// 根据i 节点信息,取数据块在设备上的对应的逻辑块号。
for (i = 0; i < 4; block++, i++)
nr[i] = bmap (current->executable, block);
// 读设备上一个页面的数据(4 个逻辑块)到指定物理地址page 处。
bread_page (page, current->executable->i_dev, nr);
// 在增加了一页内存后,该页内存的部分可能会超过进程的end_data 位置。下面的循环即是对物
理
// 页面超出的部分进行清零处理。
i = tmp + 4096 - current->end_data;
tmp = page + 4096;
while (i-- > 0)
{
tmp--;
*(char *) tmp = 0;
}
// 如果把物理页面映射到指定线性地址的操作成功,就返回。否则就释放内存页,显示内存不够
。
if (put_page (page, address))
return;
free_page (page);
oom ();
}
13、mem_init函数
----------------------------------------
物理内存初始化。
// 参数:start_mem - 可用作分页处理的物理内存起始位置(已去除RAMDISK 所占内存空间等)。
// end_mem - 实际物理内存最大地址。
// 在该版的linux 内核中,最多能使用16Mb 的内存,大于16Mb 的内存将不于考虑,弃置不用。
// 0 - 1Mb 内存空间用于内核系统(其实是0-640Kb)。
void mem_init (long start_mem, long end_mem)
{
int i;
HIGH_MEMORY = end_mem; // 设置内存最高端。
for (i = 0; i < PAGING_PAGES; i++) // 首先置所有页面为已占用(USED=100)状态,
mem_map[i] = USED; // 即将页面映射数组全置成USED。
i = MAP_NR (start_mem); // 然后计算可使用起始内存的页面号。
end_mem -= start_mem; // 再计算可分页处理的内存块大小。
end_mem >>= 12; // 从而计算出可用于分页处理的页面数。
while (end_mem-- > 0) // 最后将这些可用页面对应的页面映射数组清零。
mem_map[i++] = 0;
}
14、calc_mem函数
------------------------------------
计算内存空闲页面数并显示。
void calc_mem (void)
{
int i, j, k, free = 0;
long *pg_tbl;
// 扫描内存页面映射数组mem_map[],获取空闲页面数并显示。
for (i = 0; i < PAGING_PAGES; i++)
if (!mem_map[i])
free++;
printk ("%d pages free (of %d)\n\r", free, PAGING_PAGES);
// 扫描所有页目录项(除0,1 项),如果页目录项有效,则统计对应页表中有效页面数
,并显示。
for (i = 2; i < 1024; i++)
{
if (1 & pg_dir[i])
{
pg_tbl = (long *) (0xfffff000 & pg_dir[i]);
for (j = k = 0; j < 1024; j++)
if (pg_tbl[j] & 1)
k++;
printk ("Pg-dir[%d] uses %d pages\n", i, k);
}
}
}
完。
July、二零一一年一月六日。
-------------------------------------------------------------
本人July对本博客所有任何内容享有版权,转载或引用任何内容、资料,
请注明作者本人 July和出处。谢谢。