[PID로 process name 얻기.]란 이전 글에서 user space에서 PID로 process name을 읽는 프로그램을 작성하였다.

이번 글은 kernel space에서 PID로 process name을 얻는 코드와 그 위험성을 함께 설명한다.

사용자 영역의 파일인  /proc/%pid/cmdline을 커널 영역에서 읽어 process name을 표시하는 프로그램 작성이 목적이다.


1. kernel  space에서 pid로 process name 찾는 kgetpname.c 파일  kgetpname.c

#include <linux/fs.h>
#include <linux/syscall.h>
#include <asm/uaccess.h>

#define MAX_PROCESS_NAME 512
#define ESUCCESS	0
#define ENOPROC		1
#define ENONAME		2


int kgetProcessNameByPid(const int pid, char* name);

int kgetProcessNameByPid(const int pid, char* name)
{
    int result = ENOPROC;
    int iRead = 0;

    if (name) {
        mn_segment_t fs;
        int fd = 0;

        sprintf(name, "/proc/%d/cmdline", pid);

        // get the current segment descriptor
        fs = get_fs();

        // set the segment descriptor to kernel space
        set_fs(get_ds());

        // open file
        fd = sys_open(name, O_RDONLY, 0);

        if (fd < 0) {
            result = ENOPROC;
        }
        else {
            if ((iRead = sys_read(fd, name, MAX_FILE_NAME)) <= 0) {
                result = ENONAME;
            }
            else {
                result = ESUCCESS;
            }

            sys_close(fd);
        }

        set_fs(fs);
    }

    return result ;
}


2. kernel space과 user space의 주소공간.

kgetpname.c 프로그램에서 /proc/%pid/cmdline를 통해 process name을 구하려 할 때 주의할 사항이 있다.

리눅스 커널이 최초로 386 PC에 포팅될 때 인텔은 segment를 사용토록 하였다. 

i386 protected mode에서 segment register는 virtual-address descriptor의 인덱스로 사용되었다.

각 메모리에 대한 접근은 CS (code segment, default for code fetch), DS (data segment, default for data access), ES, FS (extra segments, useable for data access)의 하나를 사용해야 했다.

이 때 리눅스 커널 공간의 메모리 맵은  물리적 어드레스에 일대일로 매핑 된 가상 어드레스를 사용했다. 

반면에, 사용자 공간의 메모리 맵은 실행 파일에 사용된 이진 포맷으로 쓰여지고, 낮은 가상 주소를 사용 하였다.

그러므로, 시스템 콜의 실행은 사용자 공간에서 완전히 다른 메모리 맵으로 전환 요구된다.

이것은 사용자 공간과 커널 공간을 담당하는 코드 및 데이터 세그먼트에 연결된 메모리 맵에 대해 다른 디스크립터를 수행 하였다. 

요약하면 커널 영역에서 사용자 영역의 주소 공간을 접근하기 위해 아래 함수를 사용한다.

get_fs()

set_fs()


3. kernel space과 user space의 파일 액세스.

syscall handler 사용.

sys_open()

sys_read()

sys_write()

sys_close()

VFS (virtual file syste) 레벨의 함수 사용.

flip_open()

vfs_read()

vfs_write()

flop_close()

위 두 가지 모두 동작은 한다.

VFS 레벨의 함수가 속도도 빠르나 위험도는 더 크다.

위험도가 크다는 의미는 커널 내에서 잘못 사용될 때 바로 시스템을 죽이게 된다.

필자의 경우도 VFS 레벨의 함수를 사용하다 갑자기 시스템이 죽어 syscall handler로 레벨을 낮춰 사용하였다.

아직, 지식이 부족하여 그렇다.

이후 디버깅과 여러 조언을 종합하여 다음과 같은 원인을 파악할 수 있었다.


4. kernel space과 user space의 파일 액세스의 위험.

위와 같이 추가한 기능이 잘 동작하여 원하는 프로세스 이름 전체를 알 수 있었다.

문제는 약간의 동작 후 아래 커널 로그를 찍으며 갑자기 시스템이 리부팅하게 되었다.

Unable to handle kernel NULL pointer dereference at virtual address 00000012 ... [ 3149.233164] BUG: sleeping function called from invalid context at /kernel/mm/slub.c:926 [ 3149.245789] in_atomic(): 0, irqs_disabled(): 0, pid: 818, name: kswapd0

kgetProcessNameByPid()를 호출하는 커널 내 모듈 A는 정상 동작하지만 임의의 프로세스들이 watchdog 리셋이 발생하는 것이다.

즉, flip_open() 또는 sys_open()를 isr handler 등에서 호출하면 결과를 반환하는 시간이 오래 걸릴 경우 sleep에 들어가게 된다.

이때, 빠른 응답이 필요한 다른 프로세스가 동작하지 못하여 타임아웃이 발생한다.

그 결과 watchdog timeout으로 리셋이 되는 것이다.


커널 내의 코딩은 작은 실수로도 시스템을 다운시킨 다는 것을 배웠다. 

위 예제를 커널 영역에서 사용할 때는 아주 주의해야 하며 더 나은 방법이나 제안이 있으면 댓글을 남겨주시길 바란다.



저작자 표시 비영리 변경 금지
Posted by 현무랑 니니


티스토리 툴바