| Write/Read/Open 처리 과정 이해

[정리]

1. 주제

- Linux Write/Read/Open의 처리 과정에 대한 이해

2. 전체 내용 요약

1) Write

 

[1) Write]

I/O에 대해 크게 2가지로 direct i/o와 buffered i/o 나눠짐

무조건적으로 block layer를 거치는 것은 아님. buffered I/O를 하게 되면 block layer를 접근하지 않음 

buffered 되는 것들은 크게 두 가지로 data block과 meta 정보임

이 두개의 정보를 buffering하기 위해 page cache를 할당 받아 사용

앞 시간에서 배운 anonymous page와 page cache의 차이점은 아래와 같음

anonymous page : file과는 상관 없으며, stack, heap 같은 것이 예임

page cache: disk block이 유지되는 곳. data block의 내용일 수도 있고, inode 같은 meta 정보일 수도 있음

pagecache: buffers + cached, 파일 관련된 내용이 저장된 곳이며, anonymous page와 구분해 파악 필요

 

 

## 하나의 task가 anonymous page를 차지하고 있는 크기 확인 법
$ cat /proc/$$/status | grep An

$$는 현재 bash의 pid 번호를 나타내며, 현재 bash가 차지하고 있는 anonymous page의 크기를 RssAnon과 같음

 

B. Buffered write

Buffered I/O의 3단계는 아래와 같으며 이후에선 buffered I/O에 대해 실습을 진행 함

1) write_begin : pagecache 준비

2) user data copy : pagecache 쓰기

3) write_end : pagecache dirty 작업

 

<Buffered i/o write 실습>

 

<buffered i/o write 실습>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void main()
{
    FILE *fp = fopen("hello.txt", "w");

    if (fp) {
        fprintf(fp, "hello linux filesystem\n");
        fclose(fp);
        sync();
    }
}
$ sudo uftrace record -K 30 -d write.uftrace.data ./write
$ uftrace tui (기존에는 replay로 봤는데, tui로 접었다 폈다를 할 수 있음)

fwrite 부분에 sys_write가 없는 이유는? 왜 fclose 부분에 있을까?

fwrite는 sys_write를 호출 안하는 것이 아님. 상황마다 상이함. 어떤 경우에선 fclose에, 어떤 경우에는 fwrite에 sys_write가 존재 할 수 있음. printf가 compile 시점에 여러가지 방법으로 설정되듯이 sys_write도 fopen/fclose 모두에서 호출될 수 있음

generic_perform_write는 bufferd_i/o를 실행시키는 핵심 함수.

앞에서 언급된 fs의 write_begin과 write_end가 포함되어 있음

mark_buffer_dirty : dirty bit 체크 하는 부분 (data)

mark_inode_dirty : dirty bit 체크 하는 부분 (inode)

stat을 통해 file의 변경사항을 볼 수 있음

 

<위의 과정에 tracing 결과에 실제 코드로 확인 실습>

sys_write -> ... -> __vfs_write -> .. -> file->f_op->write_iter => ext4_file_write_iter를 호출 함

ext4_file_write_iter -> __generic_file_write_iter -> generic_perform_write

최종 generic_perform_write에서 write_begin과 copy_from_user 그리고 write_end가 존재한다.

 

밑의 세 가지 부분에 대해 코드 상으로 더 확인할 것이다. 

1) write_begin : ext4_da_write_begin

- page caching을 위해 page buffer가 write_begin에서 어디 부분에서 할당되는지

2) copy from user : iov_iter_copy_from_user_atomic

- copy_from_user 부분에서 실제 write가 어디에서 진행되는지

3) write_end : ext4_da_write_end

- write_end에서 data (+meta)가 어디에서 dirty가 체크 되는지 

 

1) page caching을 위해 실제 page buffer를 할당 받는 위치는create_page_buffers이다.

 

2) kmap으로 addr 할당 받은 후 memcpy_from_page를 통해 copy from user를 수행 한다. 

 

 

...

3-1) Data에 대한 dirty 체크

...

3-2) inode에 대한 dirty 체크

 

write 추적에 대한 uftrace 첨부 파일

write.uftrace.data.taraa
10.00MB

 

write.uftrace.data.tarab
10.00MB
write.uftrace.data.tarac
0.05MB

무조건적으로 block layer를 거치는 것은 아님. buffered I/O를 하게 되면 block layer를 안거친게 됨

anonymous page와 page cache의 차이점

anonymous page : file과는 상관 없으며, stack, heap 같은 것이 예임

page cache: disk block이 유지되는 곳. data block의 내용일 수도 있고, inode 같은 meta 정보일 수도 있음

 

$ cat /proc/$$/status | grep An

task가 anonymous page를 차지하고 있는 크기 확인 법

 

bufferd I/O의 3단계

1) write_begin: pagecache 준비

2) 유저 데이터 복사 : pagecache 쓰기

3) write_end: pagecache 더티

<buffered i/o write 실습>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void main()
{
    FILE *fp = fopen("hello.txt", "w");

    if (fp) {
        fprintf(fp, "hello linux filesystem\n");
        fclose(fp);
        sync();
    }
}
$ sudo uftrace record -K 30 -d write.uftrace.data ./write
$ uftrace tui (기존에는 replay로 봤는데, tui로 접었다 폈다를 할 수 있음)

fwrite 부분에 sys_write가 없는 이유는? 왜 fclose 부분에 있을까?

-> 상황마다 상이함. 어떤 경우에선 fclose에, 어떤 경우에는 fwrite에 sys_write가 존재 할 수 있음

-> fwrite는 sys_write를 호출 안하는 것이 아님

generic_perform_write는 bufferd_i/o를 실행시키는 핵심 함수

mark_buffer_dirty -> dirty bit 체크 하는 부분 (data)

mark_inode_dirty -> dirty bit 체크 하는 부분 (inode)

stat을 통해 file의 변경사항을 볼 수 있음

 

pagecache: 파일 관련된 내용이 저장된 곳

파일 정보 -> buffers, 파일 내용 -> cached

anonymous page와 구분해 파악하자

 

major fault ex) 어제 진행된 mmap 예제, 일부 영역에 있어 page fault 발생했을 경우, 근처의 page들에 대해 추가적으로 I/O를 통해 가지고 옴. ext4_read_pages(?) 부분이 있음 

minor fault ex) ??? 다시 정리가 필요함

mmap test를 할때 미리 cat mmap_test.txt 을 수행해 data를 pagecache에 올린 후 test를 진행하면 I/O를 수반하지 않기 때문에 minor fault가 됨

 

$ vim ${linux}/arch/x86/mm/fault.c::do_user_addr_fault

major fault인지 minor인지 판단해 동작 함

write에 대한 systemcall 부분

sys_write -> ... -> __vfs_write -> .. -> ext4_file_write_iter를 호출 함

 

 

fd index 0~2 : stdin/stdout/stderr

3번 부터 파일 open을 위해 할 당 가능함

struct file을 통해 file handle 가능함

 

 $ vim ${linux}/mm/filemap.c::generic_perform_write

buffed i/o이기 떄문에 fs/ext4/file.c::ext4_file_write_iter -> mm/filemap.c::generic_perform_write로 이어짐

첫번째 줄을 보면 address_space를 file로 부터 전달 받게 된다.

여기 안에서 실제 pagecache 동작을 하게 하는 write_begin, iov_iter_copy_from_user_atomic, write_end 등이 존재하게 된다.

-----------------------------------------------------------------------------------------------------------------------------------

read에 대해 실습

1. bufferd i/o

2. cache flush 후 i/o

#include <stdio.h>
#include <stdlib.h>

//hello linux filesystem\n
#define SIZE 24

void main()
{
    FILE *fp = fopen("hello.txt","r");
    char buf[BUFSIZ];

    if (fp) {
        fread(buf, SIZE, 1, fp);
        printf("%s", buf);
        fclose(fp);
    }
}
hello.txt 파일의 내용을 caching하기 위해 cat 수행
$ cat hello.txt

실험 두개의 결과

$ uftrace replay -d read.uftrace.data | grep ext4_readpages # buffed i/o
$ uftrace replay -d read2 .uftrace.data | grep ext4_readpages # cache flush

cache flush 진행된 결과에 대해서만 ext4_readpages가 확인 된다.

read 과정:

(0) 페이지 캐시 탐색 -> hit -> read

(1) 페이지 캐시 (X) miss

   -> 페이지 캐시 준비

   -> 디스크 I/O Read 요청 ext4_readpages()

   -> 블록  I/O

   -> 디바이스 드라이버 (scsi)

      -> sleep -> IRQ -> wakeup

 

코드 레벨로 확인

위와 같은 과저으로 sys_read에서 ext4_file_read_iter까지 이어짐

fine_get_page에서 mapping이 사용되는데, 이것은 address_space이며 이것 자체를 쓴 다는 것은 buffered i/o를 한다는 것임

if(!page)에서 page NULL인 경우 disk i/o를 호출한다는 것임

pagecache를 get했기 때문에 touch_atime으로 종료가 됨 즉, buffed i/o

반면에 cache miss의 경우 ext4에 의해 read를 하게 되며, 그 부분에 read_pages()->ext4_readpages()

 cache miss의 경우 read pages 요청을 block layer을 통해 진행 후 io schdule 함수를 통해 schdule을 호출한다. schdule을 호출된다는 것은 CPU 점유를 놓는다는 것이다.(disk i/o는 오래걸리는 작업이기 때문에)

 

read_ahead 수행시 한번에 읽을 크기

 

 

---------------------------------------------------------------------------------------------------------------------------------

vfs에서 분기가 되는 call 흐름

 

vfs->proc 처리 부분

$ sudo uftrace record --force -d proc.uftrace.data -K 30 cat /proc/version
$ uftrace replay -t 1ms

proc_reg_read()

---------------------------------------------------------------------------------------------------------------------------------

파일 open 과정

주된 목적: struct file을 준비 하는 것

struct file 준비 후 fd에 매핑해 전달 함

커널 file 객체 준비하는게 가장 중요함

namei.c에서 namei의 의미는 name to inode, 경로명을 inode로 변경

 

실습은 write했던 uftrace자료에서 open쪽 확인

do_sys_open에 do_file_open과 fd_install이 모두 존재하며, do_file_open은 file 객체를 준비해주는 과정, fd_install은 file객체를 fd에 넣어 주는 과정

 

do_file_open에서 namei 과정을 하게 되는데 크게 두 가지 함 1. 이름을 dentry로 변경 -> 2.  dentry를  inode로 변경

loopup_open의 역할

inode 찾은 후 없으면 create 진행되어야 함

 

f->f_inode = inode; file 객체에 inode를 연결 하는 부분

fd_install 진행되는 부분, rcu_assing_pointer() 부분이 fdt->fd[fd]에 file 객체를 연결하는 부분임

마치 fdt->fd[fd] = file;과 유사함 추가적인 scheme들이 추가되었을 뿐

 

------------------------------------------------------------------------------------------------------------------------------

파일 디스크립터 기본 개수 및 최대 개수

 

최대 오픈 가능한 파일 개수 및 하나의 task가 오픈할 수 있는 파일 개수=

열수 있는 파일 개수를 조절해 더이상 파일을 열수 없게 함

 

<최대 오픈 가능한 file개수 확인 코드>

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void main()
{
	FILE *fp[2048];
	int i, files_max;
	char filename[128];

	for (i = 0; i < 2048; i++) {
		sprintf(filename, "hello%d.txt", i);
		fp[i] = fopen(filename, "w");

		if (fp[i]) {
			fprintf(fp[i], "hello linux filesystem\n");
		} else {
			printf("failed fd %d\n", i);
			break;
		}
		printf("%s\n", filename);
	}

	files_max = i;

	for (i = 0; i < files_max; i++)
		fclose(fp[i]);
}

fd 1021까지만 출력된 이유는 총 1024개를 fd open할수 있으나, 기본적으로 stdin, stdout, stderr이 포함되어 있기 때문에 1021에서 에러가 발생한다.

 

 

------------------------------------------------------------------------------------------------------------------------------

 

리다이렉트 '>' 와 파이프 '|' 그리고 파일 디스크립터

 

echo "hello" > hello.txt

echo "hello"는 stdout에 출력하는 것임 그것을 dup2를 통해 hello.txt가 위치한 fd 3 혹은 4번에 write하게 되는 것임

dup2가 1번 fd를 4번으로 바꾼다.

 

------------------------------------------------------------------------------------------------------------------------------

 

Sync 과정

 

root@ubuntu:/sys/kernel/debug/tracing# echo 1 > events/writeback/enable
root@ubuntu:/sys/kernel/debug/tracing# echo 1 > options/stacktrace

$ sync

sync 후 kworker에 의해 wb 발생. wb == write back

  1. wb_workfn

  2. wb_do_writeback

  3. wb_writeback (trace_writeback_exec)

  4. __writeback_inodes_wb

  5. writeback_sb_inodes

  6. __writeback_single_inode (trace_writeback_single_inode_start)

  7. do_writepages

 

--------------------------------------------------------------------------------------------------------------------------------

 

VFS / FS 주요 자료 구조 & 블록 디바이스 I/O

block size와 sector 사이즈 계산하기

 

Block 처리 과정 : blktrace를 통해서 블록 처리 과정 확인

 

$ sudo btrace /dev/sda > brace.data

A->Q->M으로 끝난 요청은 request에 bio merge 후 끝

--------------------------------------------------------------------------------------------------------------------------------

아이노드 number와 블록 주소

inode 확인 및 inode를 통한 파일 명 확인

3183523은 data block 주소. 이 주소를 통해 data block의 내용을 읽자

blkcat과 cat의 결과는 같지만 cat은 file open을 통해 fd install까지 하고 read해서 출력하는 것이지만, blkcat은 direct i/o를 해서 출력하게 된다

 

<inode 찾는 스크립트>
#!/bin/bash

DEV=$1
I=$2
__STAT=$(fsstat $DEV)
_STAT=$(grep -A 4 'BLOCK GROUP INFORMATION' <<< $__STAT)
_STAT_I=$(awk '{print $NF}' <<< $(grep 'Inodes' <<< $_STAT))
_STAT_D=$(awk '{print $NF}' <<< $(grep 'Blocks' <<< $_STAT))

# Number of Inode, Data in one EXT4 group
echo "Inode/Data Blks per one Grp : $_STAT_I / $_STAT_D"

G_IDX=$[I/_STAT_I]
# Info about EXT Group
grep -A 8 "Group: $G_IDX:" <<< $__STAT
G_STAT=$(grep -A 8 "Group: $G_IDX:" <<< $__STAT)
G_I_START=$(awk '{print $3}' <<< $(grep 'Inode Range' <<< $G_STAT))
G_I_OFFSET=$((I-G_I_START))

G_I_Nth_BLK=$((G_I_OFFSET/16))
_TMP=$((G_I_Nth_BLK*16))
G_I_Nth_BLK_Idx=$((G_I_OFFSET-_TMP))

echo "Inode: $I / Group: $G_IDX / Nth Inode: $red($G_I_OFFSET) <=$I-$G_I_START"
echo "Inode BLK: $G_I_OFFSET/16=$G_I_Nth_BLK, $G_I_Nth_BLK_Idx"

G_I_BLK_START=$(awk '{print $3}' <<< $(grep 'Inode Table' <<< $G_STAT))
G_I_BLK_LOCATED=$((G_I_BLK_START+G_I_Nth_BLK))

#echo "Inode located at:" $G_I_BLK_LOCATED "+ $G_I_Nth_BLK_Idx*256 Byte"
echo "Inode located at: $G_I_BLK_LOCATED "

#OFFSET=$((G_I_Nth_BLK_Idx*256))
#echo "Dumping $G_I_BLK_LOCATED from $OFFSET"
#blkcat -h $DEV $G_I_BLK_LOCATED | grep -A 16 -w $OFFSET

hello.txt의 inode number를 통해 실제 inode block의 주소를 찾아 냄

--------------------------------------------------------------------------------------------------------------------------------

블록처리 과정 추적

 

$ sudo btrace /dev/sda > write.btrace.data 실행 후 아래 과정 수행

write는 vfs에서 실험했던 write 파일임

4418743을 *8해서 sector로 변경->35349944

btrace에서 35349944 탐색시 (8,2)에서 remap되어 (8,0)에서 실행됨 그래서 35349944이 아닌 35354040에 대해 주소 값을 찾아야 함

35354040에 대해 결과 값을 확인 한 것

 

이번에는 inode block에 대해서 확인을 해보자

<inode number로 inode 정보 확인>
#!/bin/bash

DEV=$1
I=$2
__STAT=$(fsstat $DEV)
_STAT=$(grep -A 4 'BLOCK GROUP INFORMATION' <<< $__STAT)
_STAT_I=$(awk '{print $NF}' <<< $(grep 'Inodes' <<< $_STAT))
_STAT_D=$(awk '{print $NF}' <<< $(grep 'Blocks' <<< $_STAT))

# Number of Inode, Data in one EXT4 group
echo "Inode/Data Blks per one Grp : $_STAT_I / $_STAT_D"

G_IDX=$[I/_STAT_I]
# Info about EXT Group
grep -A 8 "Group: $G_IDX:" <<< $__STAT
G_STAT=$(grep -A 8 "Group: $G_IDX:" <<< $__STAT)
G_I_START=$(awk '{print $3}' <<< $(grep 'Inode Range' <<< $G_STAT))
G_I_OFFSET=$((I-G_I_START))

G_I_Nth_BLK=$((G_I_OFFSET/16))
_TMP=$((G_I_Nth_BLK*16))
G_I_Nth_BLK_Idx=$((G_I_OFFSET-_TMP))

echo "Inode: $I / Group: $G_IDX / Nth Inode: $red($G_I_OFFSET) <=$I-$G_I_START"
echo "Inode BLK: $G_I_OFFSET/16=$G_I_Nth_BLK, $G_I_Nth_BLK_Idx"

G_I_BLK_START=$(awk '{print $3}' <<< $(grep 'Inode Table' <<< $G_STAT))
G_I_BLK_LOCATED=$((G_I_BLK_START+G_I_Nth_BLK))

#echo "Inode located at:" $G_I_BLK_LOCATED "+ $G_I_Nth_BLK_Idx*256 Byte"
echo "Inode located at: $G_I_BLK_LOCATED "

#OFFSET=$((G_I_Nth_BLK_Idx*256))
#echo "Dumping $G_I_BLK_LOCATED from $OFFSET"
#blkcat -h $DEV $G_I_BLK_LOCATED | grep -A 16 -w $OFFSET

WSM :

- W : write

- S : sync

- M : Meta data

여기에서는 inode block에 대해 2번 수정이 된 것이다. 시간 대역이 다르므로

 

 

block tracepoint를 통한 추적

$ echo 1 > events/block/enable

해당 부분들이 trace의 결과로 보여지는 것이다. 

------------------------------------------------------------------------------------------------------------------------------

리눅스 스케줄러의 이해

 

커널 버전 변경하기

sudo apt install -y linux-image-5.4.0-65-generic

reboot

cgrup들에 대해 cfs에 대해 period 값 확인 및 해당 task들에 대한 할당 CPU 총시간

process의 실행한시간 runtime 값 확인

$ cat /proc/%%/sched

 

한 프로세스가 가질 수 있는 최소한의 period 시간

-------------------------------------------------------------------------------------------------------------------

우선순위/nice 값/weight 값 이해와 실습

 

nice 값을 확인하는 방법은 원래 stat을 통해 하나씩 해결해야 함. man proc을 수행하면 어떤 값들이 어떤 의미를 가지고 있는지 알려줌

kworker의 경우 모두 우선순위기 높은 것은 아니다. 

top에서 NI가 nice 값을 나타낸다. 

 

나이스값을 변경하는 실습

원래 기본적으로 bash의 nice 값이 0이지만, nice bash를 통해 새로 bash를 생성 후 확인시 10으로 되어 있음을 확인 함

nice 값 조절도 가능함

renice를 통해 현재 사용중인 bash에 대해 nice 값을 수정 할 수 있음

 

nice에 매칭되는 weight 값

process에 할당된 nice 값과 weight값

저기에 보이는 weight 값은 *1024가 되어 있으므로 값/1024해야 함

echo $((90891264/1024)) = 88761 -> nice -20

 

--------------------------------------------------------------------------------------------------------------------------------

프로세스의 타임슬라이스(실행할 시간) 할당

 

 

 

 

 

 

 

| 커널 메모리 관리 

[정리]

1. 주제

- ㅇ

2. 전체 내용 요약

1) 커널에서 메모리 할당 하는 방법

2) buddy system 확인

3) slab allocator

4) vmalloc 할당 및 회수 과정

 

[1) 커널에서 메모리 할당 하는 방법]

총 4가지(?) 더 있을 것이며, 조사 필요

alloc_pages : 페이지 자체 할당

kmem_cache_alloc : 자주쓰는 자료구조 할당 ex) task_struct

kmalloc : 요청된 사이즈만큼 물리연속된 메모리 영역 할당. 메모리에 선 할당되어 있는 영역에서 할당을 해주기 떄문에 연속적으로 할당 할 수 있음

vmalloc : 요청된 사이즈만큼 물리적으로 연속된 메모리 영역 할당 보장 할 수 없음. 후매핑용 영역에서 할당하기 때문에 물리적으로 연속됨을 보장 할 수 없는 것임

 

다른 함수들에서 page가 필요한 경우 __get_free_pages를 호출하게 됨. 이때 몇개의 page가 필요한지 order를 통해 전달하게 되며, alloc_pages() 함수를 통해 실제 page관리 주체인 struct page를 할당 받게 됨. 

이후 struct page는 page_address()내에서 virtual address를 return하며 상위 함수에서 요청한 page를 전달하게 됨

struct page는 kernel에서 page를 관리하는 주체를 말하는 것이며, page내 vitrual 변수가 실제 메모리에 할당된 page를 가르키게 됨

즉, _get_free_pages()는 데이터를 로딩할 가상 메모리 주소를 받게 된 것이며, alloc_pages()는 실제 가상 메모리를 관리할 struct page를 할당 받게 된 것임

# cd /sys/kernel/debug/tracing
# echo 1 > events/kmem/mm_page_alloc/enable
# echo 1 > options/stacktrace
# cat trace_pipe

alloc_pages 수행시 orders를 통해 몇 page를 할당 했는지 확인 가능함. order =0 1page 할당

order에 대한 내용은 뒤에서 buddy system에서 좀 더 자세하게 나옴

 

[2) buddy system 확인]

<buddy info 보기 쉬운 python 스크립트>
#!/usr/bin/env python
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 textwidth=79 autoindent

"""
Python source code
Last modified: 15 Feb 2014 - 13:38
Last author: lmwangi at gmail  com
Displays the available memory fragments
by querying /proc/buddyinfo
Example:
# python buddyinfo.py
"""
import optparse
import os
import re
from collections import defaultdict
import logging


class Logger:
    def __init__(self, log_level):
        self.log_level = log_level

    def get_formatter(self):
        return logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

    def get_handler(self):
        return logging.StreamHandler()

    def get_logger(self):
        """Returns a Logger instance for the specified module_name"""
        logger = logging.getLogger('main')
        logger.setLevel(self.log_level)
        log_handler = self.get_handler()
        log_handler.setFormatter(self.get_formatter())
        logger.addHandler(log_handler)
        return logger


class BuddyInfo(object):
    """BuddyInfo DAO"""
    def __init__(self, logger):
        super(BuddyInfo, self).__init__()
        self.log = logger
        self.buddyinfo = self.load_buddyinfo()

    def parse_line(self, line):
        line = line.strip()
        #self.log.debug("Parsing line: %s" % line)
        parsed_line = re.match("Node\s+(?P<numa_node>\d+).*zone\s+(?P<zone>\w+)\s+(?P<nr_free>.*)", line).groupdict()
        #self.log.debug("Parsed line: %s" % parsed_line)
        return parsed_line

    def read_buddyinfo(self):
        buddyhash = defaultdict(list)
        buddyinfo = open("/proc/buddyinfo").readlines()
        for line in map(self.parse_line, buddyinfo):
            numa_node =  int(line["numa_node"])
            zone = line["zone"]
            free_fragments = map(int, line["nr_free"].split())
            max_order = len(free_fragments)
            fragment_sizes = self.get_order_sizes(max_order)
            usage_in_bytes =  [block[0] * block[1] for block in zip(free_fragments, fragment_sizes)]
            buddyhash[numa_node].append({
                "zone": zone,
                "nr_free": free_fragments,
                "sz_fragment": fragment_sizes,
                "usage": usage_in_bytes })
        return buddyhash

    def load_buddyinfo(self):
        buddyhash = self.read_buddyinfo()
        #self.log.info(buddyhash)
        return buddyhash

    def page_size(self):
        return os.sysconf("SC_PAGE_SIZE")

    def get_order_sizes(self, max_order):
        return [self.page_size() * 2**order for order in range(0, max_order)]

    def __str__(self):
        ret_string = ""
        width = 20
        for node in self.buddyinfo:
            ret_string += "Node: %s\n" % node
            for zoneinfo in self.buddyinfo.get(node):
                ret_string += " Zone: %s\n" % zoneinfo.get("zone")
                ret_string += " Free KiB in zone: %.2f\n" % (sum(zoneinfo.get("usage")) / (1024.0))
                ret_string += '\t{0:{align}{width}} {1:{align}{width}} {2:{align}{width}}\n'.format(
                        "Fragment size", "Free fragments", "Total available KiB",
                        width=width,
                        align="<")
                for idx in range(len(zoneinfo.get("sz_fragment"))):
                    ret_string += '\t{order:{align}{width}} {nr:{align}{width}} {usage:{align}{width}}\n'.format(
                        width=width,
                        align="<",
                        order = zoneinfo.get("sz_fragment")[idx],
                        nr = zoneinfo.get("nr_free")[idx],
                        usage = zoneinfo.get("usage")[idx] / 1024.0)

        return ret_string

def main():
    """Main function. Called when this file is a shell script"""
    usage = "usage: %prog [options]"
    parser = optparse.OptionParser(usage)
    parser.add_option("-s", "--size", dest="size", choices=["B","K","M"],
                      action="store", type="choice", help="Return results in bytes, kib, mib")

    (options, args) = parser.parse_args()
    logger = Logger(logging.DEBUG).get_logger()
    #logger.info("Starting....")
    #logger.info("Parsed options: %s" % options)
    #print logger
    buddy = BuddyInfo(logger)
    print buddy

if __name__ == '__main__':
    main()

$ cat /proc/buddyinfo

위의 코드 결과들에 대해 이해하기 위해 위의 자료를 보자

기본적으로 처음 시작시 order=10(4MB) 크기 단위로만 page 묶음이 존재하며,

추후 task에 의해 order 10보다 작은 크기의 page가 요청될 경우 order 10의 메모리는 분할된 후 요청된 크기에 맞게 메모리를 할당하게 됨

여기서 처음 조각을 지속적으로 분할해 사용한다고 해서 rmqueue라는 명칭을 사용함

분할 ex) 2^10 -> 2^9  * 2 형태로 분할

회수 ex) 이웃한 2^9 * 2 -> 2^10

분할 <-> 회수 의 과정이 지속적으로 반복 됨

 

$ vim ${linux}/mm/page_allo.c
struct page *rmqueue()
struct page *__rmqueue_smallest()

<rmqueue를 통해 page 분할 후 할당 하는 부분에 대해 실습이 이루어 지지 않음, 추가적으로 진행 필요>

 

 

# cd /sys/kernel/debug/tracing
$ echo 1 > events/kmem/mm_page_free/enable   
$ echo 1 > options/stacktrace
$ cat trace

$ vim ${linux}/mm/page_allo.c

page 해제는 free_pages() - > __free_one_page() 최종적으로 버디에 기반 메모리 해제 작업 수행됨

위 함수를 거쳐서 최종적으로 버디시스템 기반 메모리 해지작업 수행

 

| 가상 메모리, Page fault handling

[정리]

1. 주제

- Page fault는 왜 발생하는 것이며, page fault 발생시 kernel은 어떻게 동작 하는가

2. 전체 내용 요약

1) 이론 필기 내용 요약

A. 가상 메모리

B. Page table (Multi-level page table)

C. User / Kenrel 가상주소 범위 확인

2) mmap을 통한 page fault handling 추적

3) read syscall을 이용한 page fault handling 추적

4) mmap vs. read 비교

 

[1)이론 필기 내용 요약]

A. 가상 메모리

$ free -h

free를 통해 전체 system에서 사용중인 memory에 대해 대략적으로 확인 가능함

자세한 내용은 '실습과 그림으로 배우는 리눅스 구조' 책에서 메모리 파트에 자세하게 설명 되어 있음

total, used, free, shared, buff/chace, available에 대한 의미를 파악 해야 함

total = used + free + buff/cache

used는 anonymous page를 나타내며 programming으로 사용되는 것, heap, stack등 같은 메모리를 뜻 함

buff/cache는 disk상의 내용이 메모리상에 유지될때를 말하며, bufferd cache,  page cache를 말함

available는 free + buff/cache의 합이며, process가 처음에 최초로 실행이 된다면, 최대치로 사용할 수 있는 메모리의 양을 뜻 함

free에 보여지는 buff/cache에 대해 /proc/meminfo를 통해 분리해 볼 수 있음

buffers : inode block과 같은 파일 정보를 나타냄

cached : data block의 파일 내용을 나타냄

앞에서 언급한 메모리 영역들은 task 할당시 사용되며, 하나의 task가 생성되어 사용할 수 있는 가상 메모리 크기는  ulimit를 통해 확인이 가능하며, unlimited로 초기에 설정되어 있음

vsz는 현재 실행중인 task가 모든 메모리에 파일 내용을 로딩했을 경우 필요한 가상 메모리의 크기를 나타내며, 최대 vsz크기는 unlimited로 설정되어 있음

unlimited의 최대 크기는 2bit OS이라면 2^32, 64bit OS이라면 2^64로 결정 됨

 

B. Page table (Multi-level page table)

참조 :

http://jake.dothome.co.kr/pt64/ 

https://www.kernel.org/doc/gorman/html/understand/understand006.html

$ cat /boot/config-5.3.0 | grep PGTABLE_LEVELS

사용중인 kernel의 page table level을 확인 하는 방법이며, 위의 결과에선 4단계 multi-level page table을 사용 중

최신 kernel에서는 level 5까지 지원하는 것으로 보임

<용어>

pgd (page global directory)

pud (page upper directory)

pmd (page middle directory)

pagetable

pte (page table entry)

 

multi level page table을 사용함에 있어 64bit를 9 / 9 / 9 / 9 / 12 로 총 48bit에 대해 사용 함

처음 초기화시 pgd만 존재하며, 만약 pgd level에서 pud entry가 존재하지 않아 page fault가 발생하는 경우,

pud ~ pte까지 모든 요소에 대해 모두 메모리에 loading하게 됨

 

C. User / Kenrel 가상주소 범위 확인 (추가 정리 필요)

__do_page_fault 에서 address가 kernel영역인지 user 영역인지 확인하게 됨

그 부분을 확인시 TASK_SIZE_MAX와 비교해 판단 함

 

memory 영역 참조 자료: https://wogh8732.tistory.com/397

user와 kernel은 어떻게 메모리 영역을 나눠 사용 하는지에 대해 파악이 필요함 

여기에서 ZONE이라는 용어가 나오며 해당 자료들에 대해 조사 필요

 

[2) mmap을 통한 page fault handling 추적]

실습 코드 파일 

- hello_mmap.c : mmap을 통해 page fault 발생 시킬 예제

- mmap_test.txt : hello_mmap.c에 의해 데이터 변경될 테스트 파일

<hello_mmap.c>

#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>

#define READ_SIZE 16

int main(int argc, char **argv)
{
	int fd, flag = PROT_WRITE | PROT_READ;
	struct stat sb;
	char *addr;


	if ((fd = open(argv[1], O_RDWR|O_CREAT)) < 0) {
		printf("File Open Error\n");
		return -1;
	}

	if (fstat(fd, &sb) < 0) {
		printf("fstat error\n");
		return -1;
		
	}
	addr =  mmap(NULL, READ_SIZE, flag, MAP_SHARED, fd, 0);
	if (addr == MAP_FAILED)
		printf("mmap error\n");

	printf("%s\n", addr);
	memset(addr, 0x00, READ_SIZE);
	munmap(addr, READ_SIZE);
	close(fd);
}
<mmap_test.txt>

$ vim mmap_test.txt
111111111122222222223333333333

https://github.com/hackndev/tools/blob/master/devmem2 와 와 위의 mmap과 비교 한번 해보기

 

$ gcc  -g -pg -o hello_mmap hello_mmap.c
$ ./hello_mmap mmap_test.txt

기존에 1로 적혀 있던 부분들에 대해 hello_mmap.c에서 설정한 READ_SIZE 만큼의 크기가 0으로 memset 된 결과를 확인 할 수 있음

이 변환 과정이 동작 중 page fault를 유발할 것이며, page fault 실험을 아래처럼 진행 할 수 있음

$ sudo uftrace record -d mmap.uftrace.data -K 30 ./hello_mmap mmap_test.txt
$ cd mmap.uftrace.data 
$ uftrace replay

puts()는 memset 과정에서 page에 접근 하는 부분을 나타내고 있으며, 결과에서 보이듯이 page fault handle -> ... -> submit_bio()까지 이어져 결국 disk layer까지 연결됨을 확인 가능 함

 

mmap에서 받은 addr에 대해 memset을 통해 data write 수행시 puts()가 불리게 되며,

기존에 이미 echo 3 을 통해 disk cache flush가 되었으므로, va-pa 연결이 끊겨져있음,

따라서 puts() 수행시 page fault가 발생하고 지난번에 배운것 처럼 page fault handler 로직이 호출됨

 

우리가 만든 mmap_test.txt 파일이 ext4 fs위에서 생성되었기 때문에 __do_fault()내에서 vma->vm_ops->fault()가 ext4_filemap_fault()와 연결되는 것임. 만약 다른 fs이라면 vma에 다른 함수가 연결될 것임

 

위의 호출 과정 정리시 아래와 같음

    1) arch/x86/mm/fault.c 의 do_page_fault() 함수 부터 시작

    2) arch/x86/mm/fault.c 의 do_user_addr_fault()

    3) arch/x86/mm/fault.c 의 find_vma()

    4) mm/memory.c 의 handle_mm_fault()

    5) mm/memory.c 의 __handle_mm_fault()

    6) mm/memory.c 의 handle_pte_fault()

    7) mm/memory.c 의 do_fault()

    8) mm/memory.c 의 do_read_fault()

    9) mm/memory.c 의 __do_fault()

    10) mm/memory.c 의 vma->vm_ops->fault(vmf);

    11) fs/ext4/inode.c 의 ext4_filemap_fault()

 

[3) read syscall을 이용한 page fault handling 추적]

$ vim read.c
$ cat read.c
#include <stdio.h>
#include <stdlib.h>

#define SIZE 16

void main()
{
    FILE *fp = fopen("mmap_test.txt","r");
    char buf[BUFSIZ];

    if (fp) {
        fread(buf, SIZE, 1, fp);
        printf("%s", buf);
        fclose(fp);
    }
}
$ sudo uftrace record -d read.uftrace.data -K 30 ./read
$ uftrace replay

mmap을 했을 경우 puts()를 통해 진행되고 read의 경우 syscall에 의해서 동작하게 됨 서로 다른 call path를 가지게 됨

 

[4) mmap vs. read 비교]

작은 size 크기를 1번 읽고 그만 한다면 성능상 큰 차이는 없겠지만, 많은 양의 데이터를 연속적으로 읽는다고 했을시 caching으로 인해 mmap이 조금 더 성능이 좋음

total = used + free + buff/cache

used -> anonymous page : programming으로 사용되는 것, heap, stack등 같은

buff/cache -> disk상의 내용이 메모리상에 유지될떄, page cache를 말함

available -> free + buff/cache, process가 처음에 최초로 실행이 된다면, 최대치로 사용할 수 있는 메모리의 양

buffers : inode block과 같은 파일 정보

cached : data block의 파일 내용

pagecache -> buffers/cached 모두 나타냄

task의 최대 vsz크기는 unlimited로 설정되어 있음 -> 32bit OS이라면 2^32, 64bit OS이라면 2^64

-------------------------------------------------------------------------------------------------------------------------

Multi-level page table에 대해 이론 설명

참조 :

http://jake.dothome.co.kr/pt64/ 

https://www.kernel.org/doc/gorman/html/understand/understand006.html

 

pgd (page global directory)

pud (upper)

pmd (middle)

pagetable

pte (page table entry)

 

현재 사용중인 kernel에서 page table level을 확인 하는 방법

 

실습은 각자 하는 것으로, compile 시간이 너무 오래 걸림

-------------------------------------------------------------------------------------------------------------------------

mmap에 대한 실습 진행

#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>

#define READ_SIZE 16

int main(int argc, char **argv)
{
	int fd, flag = PROT_WRITE | PROT_READ;
	struct stat sb;
	char *addr;


	if ((fd = open(argv[1], O_RDWR|O_CREAT)) < 0) {
		printf("File Open Error\n");
		return -1;
	}

	if (fstat(fd, &sb) < 0) {
		printf("fstat error\n");
		return -1;
		
	}
	addr =  mmap(NULL, READ_SIZE, flag, MAP_SHARED, fd, 0);
	if (addr == MAP_FAILED)
		printf("mmap error\n");

	printf("%s\n", addr);
	memset(addr, 0x00, READ_SIZE);
	munmap(addr, READ_SIZE);
	close(fd);
}

https://github.com/hackndev/tools/blob/master/devmem2

와 비교해서 차이점 확인 해 보기

 

실행 방법

$ hexdump -x mmap_test.txt

-------------------------------------------------------------------------------------------------------------------------

disk cahce 비우기

mmap에 대한 trace 추적

page fault 발생해 처리하는 부분에 대해 확인할 수 있음

밑에 이어 지는 것을 보면, block layer에 request 생성 후 요청하는 부분까지 이어져있음

 

mmap에서 받은 addr에 대해 memset을 통해 data write 수행시 puts()가 불리게 되며, 

기존에 이미 echo 3 을 통해 disk cache flush가 되었으므로, va-pa 연결이 끊겨져있음,

따라서 puts() 수행시 page fault가 발생하고 지난번에 배운것 처럼 page fault handler 로직이 호출됨

 

anonymous page 정리 : https://blog.daum.net/thehalfmoon/2

mmap 수행시 fd를 안주고 NULL을 했다면, anonymous page가 되는 것임

 

우리가 만든 mmap_test.txt 파일이 ext4 fs위에서 생성되었기 때문에 __do_fault()내에서 vma->vm_ops->fault()가 ext4_filemap_fault()와 연결되는 것임. 만약 다른 fs이라면 vma에 다른 함수가 연결될 것임

 

 

이번에는 read로 직접 데이터를 읽는 것에 대해 tracing 진행

 

$ vim read.c
$ cat read.c
#include <stdio.h>
#include <stdlib.h>

#define SIZE 16

void main()
{
    FILE *fp = fopen("mmap_test.txt","r");
    char buf[BUFSIZ];

    if (fp) {
        fread(buf, SIZE, 1, fp);
        printf("%s", buf);
        fclose(fp);
    }
}

mmap을 했을 경우 puts()를 통해 진행되고 read의 경우 syscall에 의해서 동작하게 됨 서로 다른 call path를 가지게 됨

mmap vs. read 성능 비교

작은 size 크기를 1번 읽고 그만 한다면 성능상 큰 차이는 없겠지만, 많은 양의 데이터를 연속적으로 읽는다고 했을시 caching으로 인해 mmap이 조금 더 성능이 좋음

 

-------------------------------------------------------------------------------------------------------------------------

유저+커널 가상 주소 범위 알아 보기

__do_page_fault 에서 address가 kernel영역인지 user 영역인지 확인하게 됨

그 부분을 확인시 TASK_SIZE_MAX와 비교해 판단 함

 

memory 영역 참조 자료: https://wogh8732.tistory.com/397

user와 kernel은 어떻게 메모리 영역을 나눠 사용 하는지에 대해 파악이 필요함 

여기에서 ZONE이라는 용어가 나오며 해당 자료들에 대해 조사 필요

 

-------------------------------------------------------------------------------------------------------------------------

커널에서 메모리 할당 하는 방법

 

alloc_pages

kmem_cache_alloc

kmalloc -> 메모리에 선 할당되어 있는 영역에서 할당을 해주기 떄문에 연속적으로 할당 할 수 있다.

vmalloc -> 후매핑용 영역에서 할당하기 때문에 물리적으로 연속됨을 보장 할 수 없는 것임

 

struct page는 kernel에서 page를 관리하는 주체를 말하는 것이며, page내 vitrual 변수가 실제 메모리에 할당된 page를 가르키게 됨

alloc_pages 수행시 orders를 통해 몇 page를 할당 헀는지 확인 가능함. order =0 1page 할당

-------------------------------------------------------------------------------------------------------------------------

buddy system 정보 확인과 rmqueue 함수 추적

buddy info 보기 쉬운 python 스크립트

#!/usr/bin/env python
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 textwidth=79 autoindent

"""
Python source code
Last modified: 15 Feb 2014 - 13:38
Last author: lmwangi at gmail  com
Displays the available memory fragments
by querying /proc/buddyinfo
Example:
# python buddyinfo.py
"""
import optparse
import os
import re
from collections import defaultdict
import logging


class Logger:
    def __init__(self, log_level):
        self.log_level = log_level

    def get_formatter(self):
        return logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

    def get_handler(self):
        return logging.StreamHandler()

    def get_logger(self):
        """Returns a Logger instance for the specified module_name"""
        logger = logging.getLogger('main')
        logger.setLevel(self.log_level)
        log_handler = self.get_handler()
        log_handler.setFormatter(self.get_formatter())
        logger.addHandler(log_handler)
        return logger


class BuddyInfo(object):
    """BuddyInfo DAO"""
    def __init__(self, logger):
        super(BuddyInfo, self).__init__()
        self.log = logger
        self.buddyinfo = self.load_buddyinfo()

    def parse_line(self, line):
        line = line.strip()
        #self.log.debug("Parsing line: %s" % line)
        parsed_line = re.match("Node\s+(?P<numa_node>\d+).*zone\s+(?P<zone>\w+)\s+(?P<nr_free>.*)", line).groupdict()
        #self.log.debug("Parsed line: %s" % parsed_line)
        return parsed_line

    def read_buddyinfo(self):
        buddyhash = defaultdict(list)
        buddyinfo = open("/proc/buddyinfo").readlines()
        for line in map(self.parse_line, buddyinfo):
            numa_node =  int(line["numa_node"])
            zone = line["zone"]
            free_fragments = map(int, line["nr_free"].split())
            max_order = len(free_fragments)
            fragment_sizes = self.get_order_sizes(max_order)
            usage_in_bytes =  [block[0] * block[1] for block in zip(free_fragments, fragment_sizes)]
            buddyhash[numa_node].append({
                "zone": zone,
                "nr_free": free_fragments,
                "sz_fragment": fragment_sizes,
                "usage": usage_in_bytes })
        return buddyhash

    def load_buddyinfo(self):
        buddyhash = self.read_buddyinfo()
        #self.log.info(buddyhash)
        return buddyhash

    def page_size(self):
        return os.sysconf("SC_PAGE_SIZE")

    def get_order_sizes(self, max_order):
        return [self.page_size() * 2**order for order in range(0, max_order)]

    def __str__(self):
        ret_string = ""
        width = 20
        for node in self.buddyinfo:
            ret_string += "Node: %s\n" % node
            for zoneinfo in self.buddyinfo.get(node):
                ret_string += " Zone: %s\n" % zoneinfo.get("zone")
                ret_string += " Free KiB in zone: %.2f\n" % (sum(zoneinfo.get("usage")) / (1024.0))
                ret_string += '\t{0:{align}{width}} {1:{align}{width}} {2:{align}{width}}\n'.format(
                        "Fragment size", "Free fragments", "Total available KiB",
                        width=width,
                        align="<")
                for idx in range(len(zoneinfo.get("sz_fragment"))):
                    ret_string += '\t{order:{align}{width}} {nr:{align}{width}} {usage:{align}{width}}\n'.format(
                        width=width,
                        align="<",
                        order = zoneinfo.get("sz_fragment")[idx],
                        nr = zoneinfo.get("nr_free")[idx],
                        usage = zoneinfo.get("usage")[idx] / 1024.0)

        return ret_string

def main():
    """Main function. Called when this file is a shell script"""
    usage = "usage: %prog [options]"
    parser = optparse.OptionParser(usage)
    parser.add_option("-s", "--size", dest="size", choices=["B","K","M"],
                      action="store", type="choice", help="Return results in bytes, kib, mib")

    (options, args) = parser.parse_args()
    logger = Logger(logging.DEBUG).get_logger()
    #logger.info("Starting....")
    #logger.info("Parsed options: %s" % options)
    #print logger
    buddy = BuddyInfo(logger)
    print buddy

if __name__ == '__main__':
    main()

기본적으로 처음 시작시 order=10 -> 4MB 크기 단위로만 존재하며, 추후 page 요청에 의해 분할되며 크기에 맞게 메모리를 할당하게 됨 

여기서 처음 조각을 지속적으로 분할해 사용한다고 해서 rmqueue라는 명칭을 사용함

ex) 2^10 -> 2^9  * 2 형태로 분할

 

__free_one_page에서 최종적으로 버디시슽메 기반 메모리 해제 작업 수행

 

메모리 해지 free 사례 추적

mm free list를 가지고 있음. kprobe에 등록해 확인 해보자

-------------------------------------------------------------------------------------------------------------------------

slab 할당자

객체 창고?

현재 OS에서 사용하고 있는 방법

 

slab vs. slub vs. slob 차이점?

크게 slab이 동작하는 것이고 거기 안에서 세부 동작이 달라 지는 것임

vim ${linux}/include/linux/slab_def.h

vim ${linux}/inlcude/linux/slub_def.h

 

vim ${linux}/inlcude/linux/mm_types.h 내 struct page 확인시 freelist가 존재함

slub은 여러개의 page에 걸쳐 관리되는 자료구조들에 대해 freelist를 동해 연결되어 있음 

 

kmem_cache_create_usercopy()를 통해 kmem_cache_create 호출되면서 task_struct를 위한 page가 할당됨

do_fork에서 task 생성시 do_fork -> .... -> kmem_cache_alloc_node를 통해 할당

 

할당 된 부분에 대해 해제 하는 부분 kmem_cache_free()

 

-------------------------------------------------------------------------------------------------------------------------

vmalloc 과정 추적

 

echo 'p:vmalloc vmalloc' > probe_events

echo 1 > events/kprobes/vmalloc/enable

echo 1 > options/stacktrace

루프를 돌며 필요한 page 개수만큼 할당 받게 된다. 후 매핑이므로 VA-PA 연결을 해줘야 하며 그 부분은 map_vm_area에서 진행하게 된다

마스터 페이지 테이블에 같이 적어 놓는다. 그리고 다른 task가 해당 vm위치에 접근시 마스터 페이지 테이블의 내용을 복사해 사용하게 된다. 

저기에서 init_mm은 마스터 페이지 테이블을 나타낸다. 

현재 페이지 테이블에 해당 내용이 적혀 있지 않다. MMU에 대해서도 init_mm에 접근하지 못한다. 

따라서, fault가 발생할 것이다.

 

vmalloc에 대한 page fault handler 처리

 

vim arch/x86/mm/fault.c::vmalloc_fault

fault 발생한 주소가 후매핑용 vmalloc 영역이 맞는지 확인

*pdg, *pdg_k 두개가 존재하는건, 현재 페이지 테이블과, 마스터 페이지 테이블 두개를 사용하기 위함

348번 라인이 해당 의미를 타나냄. read_cr3_pa -> 현재 페이지 테이블 내용 읽기

pgd_k -> 마스터 페이지 테이블임

핵심은 355번 라인에서 마스터페이지 테이블의 내용을 현재 페이지 테이블의 내용에 복사 해옴

오프셋 계산도 결국 마스터 페이지 테이블에서 가지고 오는 것임

 

마스터 커널 페이지 테이블이 존재 하는 이유

pmd, pud, ... 모든 process들이 공유함

마스터 커널 페이지 테이블 pgd에 해당 되는 것은 전역 변수로 가지고 이씅ㅁ

이것을 따로 있는 이유는 프로세스가 생성되기도 하고 죽기도 하고 여러개가 있을텐데 그때마다

페이지테이블이 생성이 되는데 그 페이지 테이블에 대해 모두 커널영역에 ㄷ해ㅐ 각각 세팅하기엔 오버헤드이다?

 

-------------------------------------------------------------------------------------------------------------------------

 

페이지 회수과정 추적

 

LRU 같은 스킴에 의해서 현재 페이지의 개수가 High/Low/Min 기준으로 판단되어 page-out/swap-out 된다.

이것을 kswapd 프로세스가 진행하며, 개수 기준에 따라 동작을 한다. kswapd가 종료되진 않는다.

swap 유발시키는 스크립트

#!/usr/bin/python2
import numpy

result = [numpy.random.bytes(1024*1024*2) for x in xrange(1024)]  
print len(result)

python script가 page를 많이 소비하고 있으며, 그때마다 page 할당을 위해 swap한다. 이때 swapd0에 의해 swap이 발생되는 trace를 확인할 수 있었고 free 수행시 swap의 used 개수가 증가함을 확인 가능했다. 

swap 영역까지 최대로 다 사용하면 더이상 page를 할당 할 수 없는 상황이 되며, 그때 process를 강제로 OOM에 의해 종료된다. 

 

kswapd는 무한루프 진행..

 

balance_pgdat 너무 오랫동안 사용하지 않음 page들에 대해 page out 수행

kswapd_shrink_node -> shrink_node -> ... -> shrink_list

 

LRU 형태로 inactive list 멤버에 대해 정리

kswapd

 -> balance_pgdat

   -> kswapd_shrink_node

     -> shrink_node

       -> shrink_node_memcg

         -> shrink_list

           -> shrink_inactive_list

             -> shrink_page_list

               -> pageout

 

__alloc_pages

 -> __alloc_pages_nodemask

  -> __alloc_pages_slowpath

   -> __alloc_pages_direct_reclaim

    -> __perform_reclaim

     -> try_to_free_pages

      -> do_try_to_free_pages

       -> shrink_zones

        -> shrink_node

         -> shrink_node_memcg

          -> shrink_list

     -> shrink_inactive_list

      -> shrink_page_list

       -> pageout

 

 

 

 

 

| 가상 메모리, Page fault handling

 

[정리]

1. 주제

- Page fault는 왜 발생하는 것이며, page fault 발생시 kernel은 어떻게 동작 하는가

2. 전체 내용 요약

1) 이론 필기 내용 요약

2) page fault tracing 실습

3) page fault handler 과정 확인

 

 

[1) 이론 필기 내용 요약]

OS에선 physical memory를 할당 하는 것이 아닌, virtual memory를 할당 함

page fault는 VA <-> PA 매핑이 되어 있지 않기 떄문에 발생하는 것

Segmentation fault 또한 page fault의 일종

 

Virtual address를 사용시 장점

1) ABI (Application Binary Interface)

- OS, CPU, 컴파일러 등이 함께 약속된 규약 (SYSCALL, clling convension, VA, ELF ..)

- ABI를 적용함으로써 binary만 제공되어도 바로 사용이 가능한 것이다.

2) 메모리 절약

3) 보안

4) 메모리 확장 (swap disk 활용)

- file system format과는 전혀 관계 없음

 

$ ps -eo pdi,comm,vsz,rss | head -2

현재 동작중인 task에 대해 pid, command, vsz (virtual memory size), rss (resident set size)에 대해 확인

vsz는 virtual memory size를 나타내므로, 실제 task가 .bss, .data, .text 등 모든 section을 memory에 loading할시에 필요한 메모리 크기를 나타냄

 

[2) page fault tracing 실습]

kernel영역에서 발생하는 page fault에 대해 tracing

# cd /sys/kernel/debug/tracing/
# echo 1 > events/exceptions/page_fault_kernel/enable
# cat trace_pipe

(밑의 그림에서는 page_fault_kernel로 동작해 결과를 보여주지만,, 아래의 내용들은 kernel / user 유사한 부분이 많음)

address : page fault가 발생한 가상 주소

ip : page faule가 발생 했을 시점의 instruction 주소

error code :

$ vim ${linux}/arch/x86/include/asm/traps.h

위에서 발생한 error code=0x2의 의미는

error code : 0x2 = no page found + write + kernel mode access

커널 모드로 write를 수행하는데 page가 존재하지 않아 page fault가 발생함

 

이번에는 page fault가 발생한 부분에 대해 stacktrace를 사용해 call stack을 확인

기존 trace point 위치에서 실행
# echo 1 > options/stacktrace
# cat trace_pipe

copy_to_user 수행시 kernel mode에서 user 영역에 접근해 data를 write했을시 user 영역 주소가 존재하지 않아 page fault가 발생한 것임

보통 kernel 주소에 대해 page fault가 발생하는 경우는 거의 없음

 

[3) page fault handler 과정 확인]

$ vim ${linux}/arch/x86/entry/entry_64.S

page fault 발생시 page_fault가 호출되며, do_page_fault가 다시 호출되어 처리가 됨

이때, has_error_code, read_cr2는 밑의 do_page_fault함수의 argument (error_code = has_error_code, address = read_cr2)가 됨

cr2는 page fault를 일으킨 가상주소를 저장 함

$ vim ${linux}/arch/x86/mm/fault.c

page_fault -> do_page_fault -> __do_page_fault -> do_user_addr_fault의 순서로 동작

(참조: 수업 중 do_kern_addr_fault이 아닌 do_user_addr_fault에 대해서만 진행 됨)

task가 접근한 page fault 주소를 알고 있으며, 해당 address에 대해 task_struct -> mm_struct -> vma 영역 (text, stack, heap,...) 중 어느 영역에서 page fault가 발생 했는지 확인

handle_mm_fault -> .. -> __handle_mm_fault에서 VA-PA 변환테이블을 수정 함

조금 더 자세한 내용들은 뒤에 연속적으로 나옴

__handle_mm_fault -> handle_pte_fault -> do_anonymous_page에서 PA를 위한 page를 할당 받음

page 할당 이후 실제 page table entry 요소에 대해 업데이트 수행

 

위의 과정에 대해 다시 정리하자면

do_user_addr_fault()   : arch/x86/mm/fault.c

  -> handle_mm_fault()    :   mm/memory.c

      -> __handle_mm_fault()    :   mm/memory.c

          -> handle_pte_fault()     :   mm/memory.c

               -> ptep_set_access_flags()   :  arch/x86/mm/pgtable.c

                    -> set_pte() “페이지 테이블 한요소(pte) 수정”

 

 

+ Recent posts