ARM Linux Kernel 66주차
일시 : 2014.08.16 (66주차)
모임명 : NAVER개발자커뮤니티지원_IAMROOT.ORG_10차ARM-C
장소 : 토즈 강남 타워점
장소지원: NAVER 개발자 커뮤니티 지원 프로그램
참여인원: 5명
스터디 진도 :
- mm_init() 복습
- mm_init()->mem_init();
- mm_init()->kmem_cache_init();
- mm_init()->percpu_init_late();
- mm_init()->vmalloc_init();
main.c::start_kernel()
asmlinkage void __init start_kernel(void)
{
...
boot_cpu_init();
// 현재 cpu(core id)를 얻어서 cpu_XXX_bits[] 의 cpu를 셋한다.
page_address_init();
// 128개의 page_address_htable 배열을 초기화
...
setup_arch(&command_line);
...
setup_nr_cpu_ids();
setup_per_cpu_areas();
// pcpu 구조체를 만들어 줌 (mm/percpu.c)
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
// boot cpu 0의 pcpu 영역의 base주소를 core register에 설정해줌
...
page_alloc_init();
// cpu_chain에 page_alloc_cpu_notify를 연결함 (mutex lock/unlock 사용)
...
vfs_caches_init_early();
// Dentry cache, Inode-cache용 hash를 위한 메모리 공간을 각각 512kB, 256kB만큼 할당 받고,
// 131072, 65536개 만큼 hash table을 각각 만듬
...
mm_init();
// buddy와 slab 을 활성화 하고 기존 할당 받은 bootmem 은 buddy,
// pcpu 메모리, vmlist 는 slab으로 이관
// 2014/08/09 종료
- start_kernel()->mm_init()을 복습합니다. > mm_init();
main.c::mm_init()
static void __init mm_init(void)
{
page_cgroup_init_flatmem(); // null function
mem_init();
// bootmem으로 관리하던 메모리를 buddy로 이관.
// 각 section 메모리 크기를 출력.
// mm/Makefile 에서 CONFIG_SLUB 설정으로 slub.c 로 jump
kmem_cache_init();
// slub 을 활성화 시킴
percpu_init_late();
// dchunk로 할당 받은 pcpu 메모리 값들을 slab으로 카피하여 이관
pgtable_cache_init(); // null function
vmalloc_init();
// vmlist에 등록된 vm struct 들을 slab으로 이관하고 RB Tree로 구성
}
- mm_init()에서 커널의 메모리 관리자인 buddy와 slab를 활성화 하고 이후 이들로 메모리를 관리니다.
- 부트 초기에 사용하였던 메모리를 buddy와 slab으로 어떻게 복제하고 활성화하는 지 분석합니다.
- mem_init();
- kmem_cache_init();
- percpu_init_late();
- vmalloc_init();
init.c::mem_init()
void __init mem_init(void)
{
// max_pfn : 0x80000, PHYS_PFN_OFFSET: 0x20000, *mem_map: NULL
// pfn_to_page(0xA0000): page 10번째 section 주소 + 0xA0000
max_mapnr = pfn_to_page(max_pfn + PHYS_PFN_OFFSET) - mem_map;
// max_mapnr: page 10번째 section 주소 + 0xA0000
/* this will put all unused low memory onto the freelists */
free_unused_memmap(&meminfo);
// bank 0, 1에 대해 bank 0, 1 사이에 사용하지 않는 공간이 있거나
// align이 되어 있지 않으면 free_memmap을 수행
free_all_bootmem();
// bootmem으로 관리하던 메모리를 buddy로 이관.
free_highpages();
// highmem의 reserved 영역을 제외하고 buddy order 0 에 추가.
mem_init_print_info(NULL);
// 각 메모리 섹션의 정보를 구하여 출력.
#define MLK(b, t) b, t, ((t) - (b)) >> 10
#define MLM(b, t) b, t, ((t) - (b)) >> 20
#define MLK_ROUNDUP(b, t) b, t, DIV_ROUND_UP(((t) - (b)), SZ_1K)
printk(KERN_NOTICE "Virtual kernel memory layout:\n"
" vector : 0x%08lx - 0x%08lx (%4ld kB)\n"
" fixmap : 0x%08lx - 0x%08lx (%4ld kB)\n"
" vmalloc : 0x%08lx - 0x%08lx (%4ld MB)\n"
" lowmem : 0x%08lx - 0x%08lx (%4ld MB)\n"
#ifdef CONFIG_HIGHMEM // CONFIG_HIGHMEM=y
" pkmap : 0x%08lx - 0x%08lx (%4ld MB)\n"
#endif
#ifdef CONFIG_MODULES // CONFIG_MODULES=y
" modules : 0x%08lx - 0x%08lx (%4ld MB)\n"
#endif
" .text : 0x%p" " - 0x%p" " (%4d kB)\n"
" .init : 0x%p" " - 0x%p" " (%4d kB)\n"
" .data : 0x%p" " - 0x%p" " (%4d kB)\n"
" .bss : 0x%p" " - 0x%p" " (%4d kB)\n",
// CONFIG_VECTORS_BASE: 0xffff0000, PAGE_SIZE: 0x1000
// MLK(0xffff0000UL, 0xffff1000UL): 0xffff0000UL, 0xffff1000UL, 4
MLK(UL(CONFIG_VECTORS_BASE), UL(CONFIG_VECTORS_BASE) +
(PAGE_SIZE)),
// FIXADDR_START: 0xfff00000, FIXADDR_TOP: 0xfffe0000
MLK(FIXADDR_START, FIXADDR_TOP),
// VMALLOC_START: 0xf0000000, VMALLOC_END: 0xff000000
MLM(VMALLOC_START, VMALLOC_END),
// PAGE_OFFSET: 0xC0000000
MLM(PAGE_OFFSET, (unsigned long)high_memory),
#ifdef CONFIG_HIGHMEM // CONFIG_HIGHMEM=y
// PKMAP_BASE: 0xBFE00000, LAST_PKMAP: 512, PAGE_SIZE: 0x1000
MLM(PKMAP_BASE, (PKMAP_BASE) + (LAST_PKMAP) *
(PAGE_SIZE)),
#endif
#ifdef CONFIG_MODULES // CONFIG_MODULES=y
// MODULES_VADDR: 0xBF000000, MODULES_END: 0xBFE00000
MLM(MODULES_VADDR, MODULES_END),
#endif
MLK_ROUNDUP(_text, _etext),
MLK_ROUNDUP(__init_begin, __init_end),
MLK_ROUNDUP(_sdata, _edata),
MLK_ROUNDUP(__bss_start, __bss_stop));
#undef MLK
#undef MLM
#undef MLK_ROUNDUP
/*
* Check boundaries twice: Some fundamental inconsistencies can
* be detected at build time already.
*/
#ifdef CONFIG_MMU // CONFIG_MMU=y
// TASK_SIZE: 0xBF000000, MODULES_VADDR: 0xBF000000
BUILD_BUG_ON(TASK_SIZE > MODULES_VADDR);
BUG_ON(TASK_SIZE > MODULES_VADDR);
#endif
#ifdef CONFIG_HIGHMEM // CONFIG_HIGHMEM=y
// PKMAP_BASE: 0xBFE00000, LAST_PKMAP: 512, PAGE_SIZE: 0x1000, PAGE_OFFSET: 0xC0000000
BUILD_BUG_ON(PKMAP_BASE + LAST_PKMAP * PAGE_SIZE > PAGE_OFFSET);
BUG_ON(PKMAP_BASE + LAST_PKMAP * PAGE_SIZE > PAGE_OFFSET);
#endif
// PAGE_SIZE: 0x1000 (4096), get_num_physpages(): 0x80000
if (PAGE_SIZE >= 16384 && get_num_physpages() <= 128) {
// PAGE_SIZE 가 16K 보다 크고 물리 메모리가 512K 이하면 수행.
extern int sysctl_overcommit_memory;
/*
* On a machine this small we won't get
* anywhere without overcommit, so turn
* it on by default.
*/
sysctl_overcommit_memory = OVERCOMMIT_ALWAYS;
}
}
init.c::free_unuses_memmap()
- free_unused_memmap(&meminfo);
static void __init free_unused_memmap(struct meminfo *mi)
{
unsigned long bank_start, prev_bank_end = 0;
unsigned int i;
// mi: &meminfo, (&meminfo)->nr_banks: 2
for_each_bank(i, mi) {
// for (i = 0; i < (&meminfo)->nr_banks; i++)
struct membank *bank = &mi->bank[i];
// [1st] bank: &(&meminfo)->bank[0]
// [2nd] bank: &(&meminfo)->bank[1]
// [1st] bank_pfn_start(&(&meminfo)->bank[0]): 0x20000
// [2nd] bank_pfn_start(&(&meminfo)->bank[1]): 0x4f800
bank_start = bank_pfn_start(bank);
// [1st] bank_start: 0x20000
// [2nd] bank_start: 0x4f800
#ifdef CONFIG_SPARSEMEM // CONFIG_SPARSEMEM=y
// [1st] bank_start: 0x20000, prev_bank_end: 0, PAGES_PER_SECTION: 0x10000
// [1st] ALIGN(0x0, 0x10000): 0x0
// [2nd] bank_start: 0x4f800, prev_bank_end: 0x4f800, PAGES_PER_SECTION: 0x10000
// [2nd] ALIGN(0x4f800, 0x10000): 0x50000
bank_start = min(bank_start,
ALIGN(prev_bank_end, PAGES_PER_SECTION));
// [1st] bank_start: 0
// [2nd] bank_start: 0x4f800
#else
bank_start = round_down(bank_start, MAX_ORDER_NR_PAGES);
#endif
// [1st] prev_bank_end: 0, bank_start: 0
// [2nd] prev_bank_end: 0x4f800, bank_start: 0x4f800
if (prev_bank_end && prev_bank_end < bank_start)
free_memmap(prev_bank_end, bank_start);
// [1st] bank: &(&meminfo)->bank[0], MAX_ORDER_NR_PAGES: 0x400
// [1st] bank_pfn_end(&(&meminfo)->bank[0]): 0x4f800
// [2nd] bank: &(&meminfo)->bank[1], MAX_ORDER_NR_PAGES: 0x400
// [2nd] bank_pfn_end(&(&meminfo)->bank[1]): 0xa0000
prev_bank_end = ALIGN(bank_pfn_end(bank), MAX_ORDER_NR_PAGES);
// [1st] prev_bank_end: 0x4f800
// [2nd] prev_bank_end: 0xa0000
}
#ifdef CONFIG_SPARSEMEM // CONFIG_SPARSEMEM=y
// prev_bank_end: 0xa0000, PAGES_PER_SECTION: 0x10000
if (!IS_ALIGNED(prev_bank_end, PAGES_PER_SECTION))
free_memmap(prev_bank_end,
ALIGN(prev_bank_end, PAGES_PER_SECTION));
#endif
}
- bank 0, 1에 대해 bank 0, 1 사이에 사용하지 않는 공간이 있거나
- align이 되어 있지 않으면 free_memmap을 수행
bootmem.c::free_all_bootmem()
unsigned long __init free_all_bootmem(void)
{
unsigned long total_pages = 0;
bootmem_data_t *bdata;
reset_all_zones_managed_pages();
list_for_each_entry(bdata, &bdata_list, list)
// for (bdata = list_entry((&bdata_list)->next, typeof(*bdata), list);
// &bdata->list != (&bdata_list);
// bdata = list_entry(bdata->list.next, typeof(*bdata), list))
// bdata: &bdata_list, &bdata->list: (&bdata_list)->list
total_pages += free_all_bootmem_core(bdata);
// total_page: 총 free된 page 수 + 0x6
// totalram_pages: 0
totalram_pages += total_pages;
// totalram_pages: 총 free된 page 수 + 0x6
// total_page: 총 free된 page 수 + 0x6
return total_pages;
}
bootmem.c::reset_all_zones_managed_pages()
- reset_all_zones_managed_pages();
void __init reset_all_zones_managed_pages(void)
{
struct pglist_data *pgdat;
for_each_online_pgdat(pgdat)
// for (pgdat = first_online_pgdat(); pgdat; pgdat = next_online_pgdat(pgdat))
// first_online_pgdat(): &contig_page_data, pgdat: &contig_page_data
reset_node_managed_pages(pgdat);
// reset_managed_pages_done: 0
reset_managed_pages_done = 1;
// reset_managed_pages_done: 1
}
bootmem.c::free_all_bootmem_core()
- list_for_each_entry(bdata, &bdata_list, list)
- // for (bdata = list_entry((&bdata_list)->next, typeof(*bdata), list);
- // &bdata->list != (&bdata_list);
- // bdata = list_entry(bdata->list.next, typeof(*bdata), list))
- // bdata: &bdata_list, &bdata->list: (&bdata_list)->list
- total_pages += free_all_bootmem_core(bdata);
- // total_page: 총 free된 page 수 + 0x6
static unsigned long __init free_all_bootmem_core(bootmem_data_t *bdata)
{
struct page *page;
unsigned long *map, start, end, pages, count = 0;
// bdata->node_bootmem_map: (&bdata_list)->node_bootmem_map: NULL 아닌 값
if (!bdata->node_bootmem_map)
return 0;
// bdata->node_bootmem_map: (&bdata_list)->node_bootmem_map
map = bdata->node_bootmem_map;
// map: (&bdata_list)->node_bootmem_map
// bdata->node_min_pfn: (&bdata_list)->node_min_pfn: 0x20000
start = bdata->node_min_pfn;
// start: 0x20000
// bdata->node_low_pfn: (&bdata_list)->node_low_pfn: 0x4f800
end = bdata->node_low_pfn;
// end: 0x4f800
bdebug("nid=%td start=%lx end=%lx\n",
bdata - bootmem_node_data, start, end);
// "nid=0 start=0x20000 end=0x4f800"
// start: 0x20000, end: 0x4f800
while (start < end) {
unsigned long idx, vec;
unsigned shift;
// start: 0x20000
// bdata->node_min_pfn: (&bdata_list)->node_min_pfn: 0x20000
idx = start - bdata->node_min_pfn;
// idx: 0
// BITS_PER_LONG: 32
shift = idx & (BITS_PER_LONG - 1);
// shift: 0
/*
* vec holds at most BITS_PER_LONG map bits,
* bit 0 corresponds to start.
*/
// idx: 0, BITS_PER_LONG: 32
// map: (&bdata_list)->node_bootmem_map
vec = ~map[idx / BITS_PER_LONG];
// vec: ~((&bdata_list)->node_bootmem_map[0])
// shift: 0
if (shift) {
vec >>= shift;
if (end - start >= BITS_PER_LONG)
vec |= ~map[idx / BITS_PER_LONG + 1] <<
(BITS_PER_LONG - shift);
}
/*
* If we have a properly aligned and fully unreserved
* BITS_PER_LONG block of pages in front of us, free
* it in one go.
*/
// start: 0x20000, BITS_PER_LONG: 32, ~0: 0xFFFFFFFF
// vec: ~((&bdata_list)->node_bootmem_map[0])
if (IS_ALIGNED(start, BITS_PER_LONG) && vec == ~0UL) {
// node_bootmem_map[0]의 값이 0일 경우로 가정
// BITS_PER_LONG: 32, ilog2(BITS_PER_LONG): 5
int order = ilog2(BITS_PER_LONG);
// order: 5
// start: 0x20000, pfn_to_page(0x20000): 0x20000의 해당하는 struct page의 주소
__free_pages_bootmem(pfn_to_page(start), order);
// CPU0의 vm_event_states.event[PGFREE] 를 32로 설정함
// page에 해당하는 pageblock의 migrate flag를 반환함
// struct page의 index 멤버에 migratetype을 저장함
// struct page의 _count 멥버의 값을 0 으로 초기화함
// order 5 buddy를 contig_page_data에 추가함
// (&contig_page_data)->node_zones[ZONE_NORMAL].vm_stat[NR_FREE_PAGES]: 32 로 설정
// vmstat.c의 vm_stat[NR_FREE_PAGES] 전역 변수에도 32로 설정
// count: 0, BITS_PER_LONG: 32
count += BITS_PER_LONG;
// count: 32
// start: 0x20000 (pfn), BITS_PER_LONG: 32
start += BITS_PER_LONG;
// start: 0x20020
} else {
// node_bootmem_map[0]의 값이 0아닐 경우로 가정
// node_bootmem_map[0]의 값이 0x000000F0 로 가정하고 분석
// start: 0x20000
unsigned long cur = start;
// cur: 0x20000
// start: 0x20000, BITS_PER_LONG: 32
start = ALIGN(start + 1, BITS_PER_LONG);
// start: 0x20020
// vec: ~((&bdata_list)->node_bootmem_map[0]): 0xffffff0f,
// start: 0x20020, cur: 0x20000
while (vec && cur != start) {
// vec: 0xffffff0f
if (vec & 1) {
// cur: 0x20000
page = pfn_to_page(cur);
// page: 0x20000 (pfn)
// page: 0x20000 (pfn), 0
__free_pages_bootmem(page, 0);
// CPU0의 vm_event_states.event[PGFREE] 를 1로 설정함
// page에 해당하는 pageblock의 migrate flag를 반환함
// struct page의 index 멤버에 migratetype을 저장함
// struct page의 _count 멥버의 값을 0 으로 초기화함
// order 0 buddy를 contig_page_data에 추가함
// (&contig_page_data)->node_zones[ZONE_NORMAL].vm_stat[NR_FREE_PAGES]: 1 로 설정
// vmstat.c의 vm_stat[NR_FREE_PAGES] 전역 변수에도 1로 설정
// count: 0
count++;
// count: 1
}
// vec: 0xffffff0f
vec >>= 1;
// vec: 0x7fffff87
// cur: 0x20000
++cur;
// cur: 0x20001
}
// cur이 0x20000 ~ 0x20020까지 수행됨
}
}
// CPU0의 vm_event_states.event[PGFREE] 를 order로 설정함
// page에 해당하는 pageblock의 migrate flag를 반환함
// struct page의 index 멤버에 migratetype을 저장함
// order 값의 buddy를 contig_page_data에 추가함
// (&contig_page_data)->node_zones[ZONE_NORMAL].vm_stat[NR_FREE_PAGES]: 2^order 값으로 설정
// vmstat.c의 vm_stat[NR_FREE_PAGES] 전역 변수에도 2^order 로 설정
// 현재 page의 page->private값과 buddy의 page->private값이 같으면 page order를 합치는 작업 수행
// bdata->node_bootmem_map: (&bdata_list)->node_bootmem_map: NULL 아닌 값
page = virt_to_page(bdata->node_bootmem_map);
// page: bdata->node_bootmem_map (pfn)
// bdata->node_low_pfn: (&bdata_list)->node_low_pfn: 0x4f800
// bdata->node_min_pfn: (&bdata_list)->node_min_pfn: 0x20000
pages = bdata->node_low_pfn - bdata->node_min_pfn;
// pages: 0x2f800
// bootmem_bootmap_pages(0x2f800): 0x6
pages = bootmem_bootmap_pages(pages);
// pages: 0x6
// count: 총 free된 page 수
count += pages;
// count: 총 free된 page 수 + 0x6
// pages: 0x6
while (pages--)
__free_pages_bootmem(page++, 0);
// bdata->node_bootmem_map에서 사용하던 page를 free시킴
// 이제부터는 buddy로 관리
// count: 총 free된 page 수 + 0x6
bdebug("nid=%td released=%lx\n", bdata - bootmem_node_data, count);
// "nid=0 released=????"
// count: 총 free된 page 수 + 0x6
return count;
}
init.c::free_highpages()
static void __init free_highpages(void)
{
#ifdef CONFIG_HIGHMEM // CONFIG_HIGHMEM=y
// max_low_pfn: 0x4F800
unsigned long max_low = max_low_pfn;
// max_low: 0x4F800
struct memblock_region *mem, *res;
/* set highmem page free */
// memblock.memory.cnt : 1
for_each_memblock(memory, mem) {
// for (mem = memblock.memory.regions;
// mem < (memblock.memory.regions + memblock.memory.cnt); mem++)
// mem: memblock.memory.regions
unsigned long start = memblock_region_memory_base_pfn(mem);
// start: 0x20000
// mem: memblock.memory.regions
unsigned long end = memblock_region_memory_end_pfn(mem);
// end: 0xA0000
/* Ignore complete lowmem entries */
// end: 0xA0000, max_low: 0x4F800
if (end <= max_low)
continue;
/* Truncate partial highmem entries */
// start: 0x20000, max_low: 0x4F800
if (start < max_low)
start = max_low;
// start: 0x4F800
/* Find and exclude any reserved regions */
// res: memblock.reserved.regions, memblock.reserved.cnt: ??(4개 이상)
for_each_memblock(reserved, res) {
// for (res = memblock.reserved.regions;
// res < (memblock.reserved.regions + memblock.reserved.cnt); res++)
// 현재 highmem은 매핑되지 않았다.
// 가정: 0x50000 (pfn) ~ 0x50100 (pfn) highmem 영역이 reserved 되어있다.
unsigned long res_start, res_end;
// res: memblock.reserved.regions
res_start = memblock_region_reserved_base_pfn(res);
// res: memblock.reserved.regions
res_end = memblock_region_reserved_end_pfn(res);
// 가정값:
// res_start: 0x50000 (pfn), res_end: 0x50100 (pfn)
// res_end: 0x50100, start: 0x4F800
if (res_end < start)
continue;
// lowmem의 reserved 영역은 skip
// highmem의 reserved 영역만 체크
// res_start: 0x50000, start: 0x4F800
if (res_start < start)
res_start = start;
// res_start: 0x50000, end: 0xA0000
if (res_start > end)
res_start = end;
// res_end: 0x50100, end: 0xA0000
if (res_end > end)
res_end = end;
// res_start: 0x50000, start: 0x4F800
if (res_start != start)
// start: 0x4F800, res_start: 0x50000
free_area_high(start, res_start);
// page를 order 0 으로 buddy에 추가.
// totalram_pages, (&(&contig_page_data)->node_zones[1])->managed_pages, totalhigh_pages
// 변수를 free된 page 만큼 증가
// start: 0x4F800, res_end: 0x50100
start = res_end;
// start: 0x50100
// start: 0x50100, end: 0xA0000
if (start == end)
break;
}
/* And now free anything which remains */
if (start < end)
free_area_high(start, end);
// highmem에 reserved 영역이 없을땐 highmem 영역 전체를 한번에 buddy order 0 에 추가.
}
#endif
}
page_alloc.c::mem_init_print_info()
void __init mem_init_print_info(const char *str)
{
unsigned long physpages, codesize, datasize, rosize, bss_size;
unsigned long init_code_size, init_data_size;
// get_num_physpages(): 0x80000
physpages = get_num_physpages();
// physpages: 0x80000
// System.map 에서 영역 계산 가능 (이하 적용)
codesize = _etext - _stext;
datasize = _edata - _sdata;
rosize = __end_rodata - __start_rodata;
bss_size = __bss_stop - __bss_start;
init_data_size = __init_end - __init_begin;
init_code_size = _einittext - _sinittext;
/*
* Detect special cases and adjust section sizes accordingly:
* 1) .init.* may be embedded into .data sections
* 2) .init.text.* may be out of [__init_begin, __init_end],
* please refer to arch/tile/kernel/vmlinux.lds.S.
* 3) .rodata.* may be embedded into .text or .data sections.
*/
// .init.* , .init.text.* , .rodata.* 섹션은 이후 다른 섹션으로 변경되기
// 때문에 현재 section의 size를 미리 계산하여 로그용으로 사용.
#define adj_init_size(start, end, size, pos, adj) \
do { \
if (start <= pos && pos < end && size > adj) \
size -= adj; \
} while (0)
adj_init_size(__init_begin, __init_end, init_data_size,
_sinittext, init_code_size);
adj_init_size(_stext, _etext, codesize, _sinittext, init_code_size);
adj_init_size(_sdata, _edata, datasize, __init_begin, init_data_size);
adj_init_size(_stext, _etext, codesize, __start_rodata, rosize);
adj_init_size(_sdata, _edata, datasize, __start_rodata, rosize);
#undef adj_init_size
printk("Memory: %luK/%luK available "
"(%luK kernel code, %luK rwdata, %luK rodata, "
"%luK init, %luK bss, %luK reserved"
#ifdef CONFIG_HIGHMEM // CONFIG_HIGHMEM=y
", %luK highmem"
#endif
"%s%s)\n",
// PAGE_SHIFT: 12
nr_free_pages() << (PAGE_SHIFT-10), physpages << (PAGE_SHIFT-10),
codesize >> 10, datasize >> 10, rosize >> 10,
(init_data_size + init_code_size) >> 10, bss_size >> 10,
(physpages - totalram_pages) << (PAGE_SHIFT-10),
#ifdef CONFIG_HIGHMEM // CONFIG_HIGHMEM=y
totalhigh_pages << (PAGE_SHIFT-10),
#endif
str ? ", " : "", str ? str : "");
}
댓글 없음:
댓글 쓰기