proc interface & seq_file

 

proc 인터페이스 (procfs 또는 /proc)

proc 인터페이스는 다음과 같은 특징을 가지고 있다.

  • 커널 코어 및 디바이스 드라이버 개발자들이 내부 커널 정보 및 디바이스 정보를 유저 스페이스에 파일 형태로 제공한다.
  • 커널 내부에 procfs 라고 불리는 가상 파일 시스템을 제공한다. 사용자는 루트 파일 시스템에 마운트하여 사용할 수 있다.
  • procfs는 커널 메모리에서만 구성되므로 빠른 접근이 가능하다.
  • 대부분의 proc 파일 정보는 사용자가 쉽게 읽을 수 있도록 readable 텍스트 형태로 출력한다. (일부는 binary로만 제공하는 정보도 있다.)

 

 

seq_file 인터페이스

  • seq_file 인터페이스는 개발자 편의를 위해 proc 인터페이스에서 파일 출력의 iteration을 간편히 제공할 목적으로 설계되었다.

 

커널 옵션

  • CONFIG_PROC_FS
    • proc 파일 시스템을 사용하기 위해서는 이 옵션이 필요한다.
    • 만들어진 proc 파일 시스템(procfs)은 마운트하여 사용한다.
  • CONFIG_PROC_KCORE
    • ELF 포맷을 지원한다. 이 파일은 gdb나 ELF 툴을 사용하여 읽어낼 수 있다.
  • CONFIG_PROC_VMCORE
    • 크래시 커널 덤프 이미지를 elf 포맷으로 출력한다.
  • CONFIG_PROC_SYSCTL
    • /proc/sys을 통해 sysctl 인터페이스를 지원한다.
  • CONFIG_PROC_PAGE_MONITOR
    • 프로세스의 메모리 사용에 대한 모니터링을 할 수 있게 한다.
    • /proc/<pid>/
      • smaps
      • clear_refs
      • pagemap
    • /proc/kpagecount
    • /proc/kpageflags

 

 

proc 인터페이스 코어

proc 파일 시스템의 코어는 아래 그림과 같이 3개의 파일을 통해 구현되어 있다.

 

샘플 proc 인터페이스 개발

자신이 개발한 디바이스 드라이버에 proc 인터페이스를 추가하여 관련 정보를 간단히 출력할 수 있는 방법을 알아보자.

  • 커널 v3.10 버전부터 proc 인터페이스가 리팩토링되었다.
  • 커널 v3.10 이전 버전에서는 proc_create() 대신 create_proc_entry() 함수를 사용하였었는데 그 방법은 생략한다.

 

 

Makefile 준비

먼저 테스트용 디렉토리를 만들고 Makefile을 준비한다.  실전에서 사용할 수 있도록 다음과 같이 두 개의 소스를 구분하였다.

  • proc 인터페이스와 관계없이 모듈의 데이터 부분을 다루는 foo.c
  • proc 인터페이스를 만들고 출력하는 proc.c

 

모듈 프로그래밍을 자주해본 경험이 있다면 다음과 같이 make -C 플래그 옵션과 -M 플래그 옵션이 어떠한 일을 하는지 알 수 있다.

  • -C: 커널 위치(include 파일들과 버전이 서로 같은지 확인해서 진행해야 하므로 모듈 컴파일 시 반드시 필요하다)
  • -M: 현재 모듈 소스를 가리키기 위해 보통 현재 위치를 가리키는 $(PWD)를 사용한다.

 

$ mkdir foo-proc
$ cd foo-proc
$ cat Makefile
obj-m := foo-proc.o
foo-proc-objs := foo.o proc.o

all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

 

소스-1(foo.c, foo.h) 준비

foo.c 함수는 모듈에 대한 라이센스 및 개발자 정보를 기록하는 부분이 있고 그 후 다음과 같이 두 개의 파트로 구분되어 있다.

  • 첫 번째 파트:
    • 샘플 데이터 생성 및 삭제를 다룬 함수들이다.
    • 가상의 구조체 데이터 2개를 만들고 리스트에 넣는 과정이다.
  • 두 번째 파트:
    • 모듈 초기화 및 종료를 다룬 함수들이다.

 

다음 그림은 모듈 로딩시에 foo_info 구조체 2개에 샘플 데이터를 담아 foo_list에 추가한 과정을 보여준다.

 

foo-proc/foo.c – 1/2

#include <linux/slab.h>
#include <linux/module.h>       /* for module programming */
#include "foo.h"
#include "proc.h"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Youngil, Moon <jake9999@dreamwiz.com>");
MODULE_DESCRIPTION("A sample driver");
MODULE_LICENSE("Dual BSD/GPL");

/*--------------------------------------------------------*/
/* 1) Generate sample data                                */
/*--------------------------------------------------------*/

DEFINE_MUTEX(foo_lock);
LIST_HEAD(foo_list);

static int add_data(int a, int b)
{
        struct foo_info *info;

        printk(KERN_INFO "%s %d, %d\n", __func__, a, b);

        info = kzalloc(sizeof(*info), GFP_KERNEL);
        if (!info)
                return -ENOMEM;

        INIT_LIST_HEAD(&info->list);
        info->a = a;
        info->b = b;

        mutex_lock(&foo_lock);
        list_add(&info->list, &foo_list);
        mutex_unlock(&foo_lock);

        return 0;
}

static int add_sample_data(void)
{
        if (add_data(10, 20))
                return -ENOMEM;
        if (add_data(30, 40))
                return -ENOMEM;
        return 0;
}

static int remove_sample_data(void)
{
        struct foo_info *tmp;
        struct list_head *node, *q;

        list_for_each_safe(node, q, &foo_list){
                tmp = list_entry(node, struct foo_info, list);
                list_del(node);
                kfree(tmp);
        }

        return 0;
}

 

아래 __init 및 __exit 부분에서 컴파일 에러가 발생하면 제거하고 사용해도 된다. (커널 옵션에 따라 결정된다)

foo-proc/foo.c – 2/2

/*--------------------------------------------------------*/
/* 2) Module part                                         */
/*--------------------------------------------------------*/

static int __init foo_init(void)
{
        if (add_sample_data())
        {
                printk(KERN_INFO "add_sample_data() failed.\n");
                return -ENOMEM;
        }

        return foo_proc_init();
}

static void __exit foo_exit(void)
{
        remove_sample_data();
        foo_proc_exit();

        return;
}

module_init(foo_init);
module_exit(foo_exit);

 

foo.h 헤더 파일에서는 2 개의 정수를 담고 foo_list라는 이름의 리스트로 연결될 수 있도록 최대한 간단히 표현하였다. foo_lock 뮤텍스는 리스트에 추가하거나 제거할 때 사용하기 위한 동기화 함수이다. (아래 예제에서는 실제 데이터 접근에 대해 동기화 할 필요 없는 상황이라 뮤텍스 코드는 제거해도 된다.  그냥 실전과 같이 리스트를 보호하기 위해 습관적으로 사용하였다.

foo-proc/foo.h

#ifndef _FOO_H_
#define _FOO_H_

#include <linux/mutex.h>
#include <linux/list.h>

struct foo_info {
        int a;
        int b;
        struct list_head list;
};

extern struct mutex foo_lock;
extern struct list_head foo_list;

#endif /* _FOO_H_ */

 

소스-2(proc.c, proc.h) 준비

proc 인터페이스를 구현하기 위해서는  파일 인터페이스(file 구조체를 사용하고, 오페레이션 구현을 위해 file_operations 구조체 사용)를 사용해야 한다. 랜덤(llseek)하게 데이터에 접근하여 데이터를 출력할 필요가 없고, 순차 처리된 데이터에만 접근하여 처리해도 되는 경우 파일 인터페이스에 끼워 간단하게 규격화하여 처리할 수 있는 새로운 방법이 있다. 여기에서는 이 새로운 시퀀스 파일 인터페이스(seq_file 구조체를 사용하고, 오퍼레이션 구현을 위해 seq_operations 구조체 사용)를 소개한다.  아울러 시퀀스 파일 인터페이스를 사용하지만 시퀀스 데이터에 대한 이동에 전혀 관여하지 않고, 단순하게 한 번의 함수 호출에서 데이터를 처리하길 바란다면 시퀀스 오퍼레이션을 구현하지 않을 수도 있다. 이 방법은 single_open() 함수를 사용하는 방법으로 proc 인터페이스 구현 중 가장 단순한 방법이다.

 

proc 인터페이스를 구현하는 방법은 다음 3가지로 구분할 수 있다. (사용자들은 구현이 간편한 시퀀스 파일 인터페이스를 가장 많이 사용한다. 따라서 샘플 코드는 B 방법과 C 방법만을 소개한다)

  • A) file_operations를 구현하여 사용하는 방법
    • 랜덤하게 데이터에 접근하여 데이터를 반복 처리할 수 있도록 iteration을 지원한다.
    • 시작(open) -> llseek -> show -> llseek -> show -> llseek -> show -> 끝(close)
  • B) file_operations 에 단순히 시퀀스 파일(seq_file) 인터페이스를 연결하고 seq_operations를 구현하여 구현하여 사용하는 방법
    • 단방향 순서대로만 데이터에 접근하여 반복 처리할 수 있는 iteration만을 지원한다.
    • 시작(open) -> next -> show -> next -> show -> next -> show -> 끝(close)
  • C) file_operations에 단순히 시퀀스 파일(seq_file) 인터페이스를  연결하고 seq_operatons는 사용하지 않는 single_open()을 사용하여 처리하는 방법
    • 반복 처리할 수 있는 iteration을 지원하지 않고, 단순히 한 번의 출력 함수 호출을 통해서 처리하므로 구현이 가장 간단하다.
    • 시작(open) -> show -> 끝(close)

 

다음 그림은 시퀀스 operations를 사용한 방법과 single_open() 방식을 사용한 방법의 처리 차이를 보여준다.

 

다음 코드는 시퀀스 오퍼레이션 처리에 관련된 코드들이다. 만일 simple_open() 방식을 사용하고자하면 #define USE_SINGLE_OPEN을 선언하여 seq_file에 대한 seq_operations 구현을 사용하지 않게 한다.

foo-proc/proc.c – 1/3.

// #define USE_SINGLE_OPEN

/*--------------------------------------------------------*/
/* 1) seq_file operations part                            */
/*--------------------------------------------------------*/
#ifdef USE_SINGLE_OPEN
#else
static void *foo_seq_start(struct seq_file *s, loff_t *pos)
{
        printk(KERN_INFO "%s", __func__);
        mutex_lock(&foo_lock);
        s->private = "";

        return seq_list_start(&foo_list, *pos);
}

static void *foo_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
        printk(KERN_INFO "%s", __func__);
        s->private = "\n";

        return seq_list_next(v, &foo_list, pos);
}

static void foo_seq_stop(struct seq_file *s, void *v)
{
        mutex_unlock(&foo_lock);
        printk(KERN_INFO "%s", __func__);
}

static int foo_seq_show(struct seq_file *m, void *v)
{
        struct foo_info *info = list_entry(v, struct foo_info, list);

        printk(KERN_INFO "%s", __func__);
        seq_printf(m, "%d + %d = %d\n", info->a, info->b, info->a + info->b);

        return 0;
}

static const struct seq_operations foo_seq_ops = {
        .start  = foo_seq_start,
        .next   = foo_seq_next,
        .stop   = foo_seq_stop,
        .show   = foo_seq_show
};
#endif

 

file_opeations를 구현에 seq_file 인터페이스를 사용하였다. 이 코드에서도 USE_SINGLE_OPEN 선언을 사용하는 경우 시퀀스 오퍼레이션을 사용하지 않음을 알 수 있다.

foo-proc/proc.c – 2/3

/*--------------------------------------------------------*/
/* 2) proc operations part                                */
/*--------------------------------------------------------*/

#ifdef USE_SINGLE_OPEN
static int foo_simple_show(struct seq_file *s, void *unused)
{
        struct foo_info *info;

        list_for_each_entry(info, &foo_list, list)
                seq_printf(s, "%d + %d = %d\n", info->a, info->b, info->a + info->b);

        return 0;
}
#endif

static int foo_proc_open(struct inode *inode, struct file *file)
{
#ifdef USE_SINGLE_OPEN
        return single_open(file, foo_simple_show, NULL);
#else
        return seq_open(file, &foo_seq_ops);
#endif
}


static const struct file_operations foo_proc_ops = {
        .owner          = THIS_MODULE,
        .open           = foo_proc_open,
        .read           = seq_read,
        .llseek         = seq_lseek,
        .release        = seq_release,
};

 

아래 파트는 /proc 디렉토리에 하위 디렉토리 및 이 모듈에 연결된 파일을 생성하는 과정을 보여준다.

foo-proc/proc.c – 3/3

/*--------------------------------------------------------*/
/* 3) proc interface part  (/proc/foo-dir/foo)            */
/*--------------------------------------------------------*/

#define FOO_DIR "foo-dir"
#define FOO_FILE "foo"

static struct proc_dir_entry *foo_proc_dir = NULL;
static struct proc_dir_entry *foo_proc_file = NULL;

int foo_proc_init(void)
{
        foo_proc_dir = proc_mkdir(FOO_DIR, NULL);
        if (foo_proc_dir == NULL)
        {
                printk("Unable to create /proc/%s\n", FOO_DIR);
                return -1;
        }

        foo_proc_file = proc_create(FOO_FILE, 0, foo_proc_dir, &foo_proc_ops); /* S_IRUGO */
        if (foo_proc_file == NULL)
        {
                printk("Unable to create /proc/%s/%s\n", FOO_DIR, FOO_FILE);
                remove_proc_entry(FOO_DIR, NULL);
                return -1;
        }

        printk(KERN_INFO "Created /proc/%s/%s\n", FOO_DIR, FOO_FILE);
        return 0;
}

void foo_proc_exit(void)
{
        /* remove directory and file from procfs */
#if LINUX_VERSION_CODE < KERNEL_VERSION(4,0,0)
        remove_proc_entry(FOO_FILE, foo_proc_dir);
        remove_proc_entry(FOO_DIR, NULL);
#else
        remove_proc_subtree(FOO_DIR, NULL);
#endif

        /* remove proc_dir_entry instance */
        proc_remove(foo_proc_file);
        proc_remove(foo_proc_dir);

        printk(KERN_INFO "Removed /proc/%s/%s\n", FOO_DIR, FOO_FILE);
}

 

foo-proc/proc.h – 3/3

#ifndef _PROC_H_
#define _PROC_H_

int foo_proc_init(void);
void foo_proc_exit(void);

#endif /* _PROC_H */

 

빌드

먼저 커널 v3.10 및 4.10 버전을 사용하는 시스템에서 빌드를 하고 테스트를 해보았다.

$ ls
Makefile  foo.c  foo.h  proc.c  proc.h

$ make
make -C /lib/modules/4.10.0-42-generic/build M=/home/jake/workspace/test/foo-proc modules
make[1]: Entering directory `/usr/src/linux-headers-4.10.0-42-generic'
  CC [M]  /home/jake/workspace/test/foo-proc/foo.o
  CC [M]  /home/jake/workspace/test/foo-proc/proc.o
  LD [M]  /home/jake/workspace/test/foo-proc/foo-proc.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/jake/workspace/test/foo-proc/foo-proc.mod.o
  LD [M]  /home/jake/workspace/test/foo-proc/foo-proc.ko
make[1]: Leaving directory `/usr/src/linux-headers-4.10.0-42-generic'

$ ls
Makefile  Module.symvers  foo-proc.ko  foo-proc.mod.c  foo-proc.mod.o  foo-proc.o  foo.c  foo.h  foo.o  modules.order  proc.c  proc.h  proc.o

 

테스트

빌드가 끝났 후 생성된 foo-proc.ko 커널 모듈을 로딩해보고 생성된 /proc/foo-dir/foo 파일을 출력해본다. 마지막으로 언로딩한다.

$ sudo insmod foo-proc.ko
$ cat /proc/foo-dir/foo
30 + 40 = 70
10 + 20 = 30
$ sudo rmmod foo-proc.ko

 

먼저 보여줄 다음은 시퀀스 오퍼레이션을 처리한 방법이다.(proc.c 파일에서 USE_SINGLE_OPEN 미사용) 커널 로그를 보고 호출되는 두 건의 데이터를 통해 foo_seq_show() 함수가 두 번 호출됨을 알 수 있다.

$ dmesg
add_data 10, 20
add_data 30, 40
Created /proc/foo-dir/foo
foo_seq_start
foo_seq_show
foo_seq_next
foo_seq_show
foo_seq_next
foo_seq_stop
foo_seq_start
foo_seq_stop
Removed /proc/foo-dir/foo

 

simple_open() 사용 방법으로 테스트

이 번 방법은 single_open() 방식이다. 먼저 proc.c 에서 USE_SINGILE_OPEN 선언을 사용하는 것으로 수정한 후, 다시 빌드한다. 그 후 생성된 모듈을 로딩한 후 테스트 해보자.  다음 커널 로그를 보는 것과 같이 foo_simple_show() 함수가 한 번만 호출된 것을 알 수 있다.

$ dmesg
add_data 10, 20
add_data 30, 40
Created /proc/foo-dir/foo
foo_simple_show
Removed /proc/foo-dir/foo

 

참고

proc_root_init()

 

proc 파일 시스템 준비

다음 그림은 proc 파일 시스템의 초기화 및 마운트에 대한 함수 처리 흐름이다.

 

proc_root_init()

fs/proc/root.c

void __init proc_root_init(void)
{
        int err;

        proc_init_inodecache();
        err = register_filesystem(&proc_fs_type);
        if (err)
                return;

        proc_self_init();
        proc_thread_self_init();
        proc_symlink("mounts", NULL, "self/mounts");

        proc_net_init();

#ifdef CONFIG_SYSVIPC
        proc_mkdir("sysvipc", NULL);
#endif
        proc_mkdir("fs", NULL);
        proc_mkdir("driver", NULL);
        proc_mkdir("fs/nfsd", NULL); /* somewhere for the nfsd filesystem to be mounted */
#if defined(CONFIG_SUN_OPENPROMFS) || defined(CONFIG_SUN_OPENPROMFS_MODULE)
        /* just give it a mountpoint */
        proc_mkdir("openprom", NULL);
#endif
        proc_tty_init();
        proc_mkdir("bus", NULL);
        proc_sys_init();
}

마운트하여 사용할 수 있도록 커널 내부에 proc 루트 파일 시스템을 생성하고 초기화한다.

  • 코드 라인 5에서 proc_inode 구조체를 자주 사용하므로 이의 kmem 캐시를 준비한다.
  • 코드 라인 6~7에서 “proc” 파일 시스템을 등록한다. 파일 시스템들은 name을 유니크 키로 사용하는 전역 file_systems(next로 연결된다)에 단방향으로 등록순으로 연결된다.
  • 코드 라인 9에서 inode용 정수 id를 발급받아 self_inum에 대입한다.
    • inode용 IDA는 전역 proc_inum_ida를 통해 관리된다.
  • 코드 라인 10에서 inode용 정수 id를 발급받아 thread_self_inum에 대입한다.
  • 코드 라인 11에서 “self/mounts”를 가리키도록 mounts라는 이름으로 스태틱 링크를 /proc에 생성한다.
    • 예) # ls /proc/mounts -la
      lrwxrwxrwx 1 root root 11 Apr 16 16:55 /proc/mounts -> self/mounts
  • 코드 라인 13에서 “self/net”를 가리키도록 net라는 이름으로 스태틱 링크를 /proc에 생성한다. 그런 후 네트워크 네임스페이스 서브시스템에 등록한다.
    • 예) # ls /proc/net -la
      lrwxrwxrwx 1 root root 8 Apr 16 16:56 /proc/net -> self/net
  • 코드 라인 15~17에서 /proc/sysvipc 디렉토리를 생성한다.
  • 코드 라인 18에서 /proc/fs 디렉토리를 생성한다.
  • 코드 라인 19에서 /proc/driver 디렉토리를 생성한다.
  • 코드 라인 20에서 /proc/nfsd 디렉토리를 생성한다.
  • 코드 라인 21~24에서 /proc/openprom 디렉토리를 생성한다.
  • 코드 라인 25에서 /proc/tty 디렉토리를 생성하고 그 하위 디렉토리에 다음을 추가로 생성한다.
    • /proc/tty/ldisc 및 /proc/tty/driver 디렉토리를 생성한다.
    • /proc/drivers 및 /proc/ldiscs 라는 이름으로 파일을 생성하고 proc 동작이 되도록 파일 오퍼레이션을 연결한다.
  • 코드 라인 26에서 /proc/bus 디렉토리를 생성한다.
  • 코드 라인 27에서 /proc/sys 디렉토리를 생성하고 proc 디렉토리 오페레이션이 되도록 연결한다. 그런 후 그 하위 디렉토리에 다음 디렉토리들을 추가로 생성하고 해당 디렉토리 내에 관련 proc 파일들을 연결한다.
    • /proc/sys/kernel 디렉토리 및 하위 proc 파일들
    • /proc/sys/vm 디렉토리 및 하위 proc 파일들
    • /proc/sys/fs 디렉토리 및 하위 proc 파일들
    • /proc/sys/debug 디렉토리 및 하위 proc 파일들
    • /proc/sys/dev 디렉토리 및 하위 proc 파일들

 

register_filesystem()

fs/filesystems.c

/**
 *      register_filesystem - register a new filesystem
 *      @fs: the file system structure
 *
 *      Adds the file system passed to the list of file systems the kernel
 *      is aware of for mount and other syscalls. Returns 0 on success,
 *      or a negative errno code on an error.
 *
 *      The &struct file_system_type that is passed is linked into the kernel 
 *      structures and must not be freed until the file system has been
 *      unregistered.
 */

int register_filesystem(struct file_system_type * fs)
{
        int res = 0;
        struct file_system_type ** p;

        BUG_ON(strchr(fs->name, '.'));
        if (fs->next)
                return -EBUSY;
        write_lock(&file_systems_lock);
        p = find_filesystem(fs->name, strlen(fs->name));
        if (*p)
                res = -EBUSY;
        else
                *p = fs;
        write_unlock(&file_systems_lock);
        return res;
}

EXPORT_SYMBOL(register_filesystem);

file_system_type 구조체를 전역 &file_systems의 마지막에 추가한다. name(이름)에 “.”이 있거나 이미 사용한 이름이 있는 경우 에러가 반환된다.

 

아래 그림은 rpi2 시스템에 등록되는 파일 시스템들을 보여준다. 그 외에 arm64에서 사용되는 hgetlbfs 및 임베디드 시스템의 플래시 파일 시스템으로 자주 사용되는 jffs2 파일 시스템도 별도로 표기하였다. (그 외의 파일 시스템은 자주 사용하지 않으므로 생략)

 

proc 파일 시스템 마운트

proc_mount()

fs/proc/root.c

static struct dentry *proc_mount(struct file_system_type *fs_type,
        int flags, const char *dev_name, void *data)
{
        int err;
        struct super_block *sb;
        struct pid_namespace *ns;
        char *options;

        if (flags & MS_KERNMOUNT) {
                ns = (struct pid_namespace *)data;
                options = NULL;
        } else {
                ns = task_active_pid_ns(current);
                options = data;

                if (!capable(CAP_SYS_ADMIN) && !fs_fully_visible(fs_type))
                        return ERR_PTR(-EPERM);

                /* Does the mounter have privilege over the pid namespace? */
                if (!ns_capable(ns->user_ns, CAP_SYS_ADMIN))
                        return ERR_PTR(-EPERM);
        }

        sb = sget(fs_type, proc_test_super, proc_set_super, flags, ns);
        if (IS_ERR(sb))
                return ERR_CAST(sb);

        if (!proc_parse_options(options, ns)) {
                deactivate_locked_super(sb);
                return ERR_PTR(-EINVAL);
        }

        if (!sb->s_root) {
                err = proc_fill_super(sb);
                if (err) {
                        deactivate_locked_super(sb);
                        return ERR_PTR(err);
                }

                sb->s_flags |= MS_ACTIVE;
        }

        return dget(sb->s_root);
}

커널에 등록된 proc 파일 시스템을 마운트한다.

  • 코드 라인 9~22에서 다음 커널 경로를 통해 호출되어 MS_KERNMOUNT 플래그가 붙은 경우 인자로 전달 받은 ns(네임 스페이스)를 알아온다. 그 외의 경우 태스크에서 사용되는 ns를 알아온다.
    • mq_init_ns(), pid_ns_prepare_proc() 및 init_hugetlbfs_fs() 함수를 사용하는 경우 다음 경로를 통해 ns를 포함하여 커널 마운트 플래그가 사용된다.
      • ns 사용: kern_mount_data() -> vfs_kern_mount() -> mount_fs()
    • simple_pin_fs() 함수를 사용하는 경우 다음 커널 경로를 통해 ns에 null이 대입된 상태로 커널 마운트 플래그가 사용된다.
      • ns=null: vfs_kern_mount() -> mount_fs()
  • 코드 라인 24~26에서 인자로 요청한 동일한 파일 시스템 타입의 수퍼 블럭들이 proc_test_super() 함수의 결과와 동일하면 proc_set_super() 함수를 호출한다.
    • proc_test_super() 함수를 통해 ns와 동일한 수퍼블럭인지 체크
    • proc_set_super() 함수를 통해 해당 수퍼 블럭을 가상 파일 시스템에 사용하는 anon 타입으로 할당하고 수퍼 블럭의 s_fs_info에 pid 값을 지정한다.
  • 코드 라인 28~31에서 커널 마운트가 아닌 경우 인자로 전달 받은 옵션 문자열을 파싱한다. proc 파일 시스템은 다음 두 개의 옵션을 파싱하여 사용한다.
    • “hidepid=<0~2>”
      • 0: 모든 유저에게 /proc/<pid>/*이 다 보이고 읽을 수 있는 커널 디폴트 설정이다.
      • 1: 모든 유저에게 /proc/<pid>/*이 다 보이지만 owner 유저만 읽을 수 있는 설정이다.
      • 2: owner 유저만 /proc/<pid>/*이 보이고 읽을 수 있는 설정이다.
    • “gid=XXX”
      • hidepid=0을 사용하면서 지정된 그룹이 모든 프로세스 정보를 수집할 수 있게 한다.
    • 참고:
  • 코드 라인 33~41에서 수퍼 블록에서 루트가 지정되지 않은 루트 inode를 생성하고 초기화한 후 지정한다.
  • 코드 라인 43에서 루트 inode에 대한 참조 카운터를 증가시키고 리턴한다.

 

다음 그림은 proc_mount() 함수에 도달하는 과정과 그 이후의 처리를 보여준다.

 

참고

[리눅스 인사이드] 2018년 5월 스터디 모집공고입니다.

안녕하세요? 문c 블로그의 문영일입니다.

 

저랑 같이 arm64 및 디바이스 드라이버 등을 스터디할 멤버를 충원합니다. 아래 사이트를 클릭하시면 스터디 공고를 확인할 수 있습니다.

 

스터디 요약

  • 참여하실 분들은 최소한 arm 커널에 대한 일부 지식이 필요합니다.
  • 처음 커널을 스터디하고자 하시는 분들은 iamroot 사이트에서 지원하는 스터디를 이용하시기 바랍니다.
  • 스터디 참여 접수 기간: 현재 ~ 2018년 5월 5일까지
  • 스터디 시작: 2018년  5월 12일 부터 (매주 토요일 오후 3시 ~ 오후 10시까지)
  • 스터디 공간: 강남 지역 사설 스터디룸을 이용하므로 매주 약 1만원 이내의 비용 발생

 

주최 사이트

  • 사이트명: 리눅스 인사이드 (Linux Inside)
  • 사이트 URL: www.linuxinside.kr
  • 대표 관리자: 윤창호
  • arm64 커널 스터디 진행: 문영일

 

참여하실 분은 아래를 클릭하셔서 참여방법을 확인하세요.

 

감사합니다.

IPC between Kernel & User space

커널 프로그래밍을 위해 커널에서 직접 제공하는 Kernel API들이 있음을 잘 알고 있을 것이다. 그 외에 유저 영역에서 커널과 인터페이스하기 위한 여러가지 수단들을 알아본다.

 

IPC between Kernel & User space

  • Posix Syscall
    • Software 인터럽트를 이용한 System Call
    • Posix library를 통해 호출
  • Kernel-provided User Helper
    • 고속 처리를 목적으로 syscall을 통하지 않고 커널 코드에 직접 호출하는 특정 API들 (특정 아키텍처에서만 지원)
  • Usermode Helper
    • 커널에서 직접 유저 코드를 로드하여 실행시킬 수 있다.
  • procfs 인터페이스
    • 리눅스 커널에 포함된 가상 /proc 디렉토리를 통해 커널 정보를 간단히 조회할 수 있다.
  • sysfs 인터페이스
    • 리눅스 커널에 포함된 가상 /sys 디렉토리를 통해 다양한 장치 드라이버 및 커널 정보에 접근할 수 있다. (커널 디버깅 및 트레이싱 인터페이스 포함)
  • cgroup 인터페이스
    • CPU 시간, 시스템 메모리, 네트워크 대역폭과 커널 자원들을 사용자 정의 작업 그룹간에 할당할 수 있다.
  • dev 인터페이스
    • 리눅스 커널에서 사용하는 장치 디바이스에 접근할 수 있다.
  • socket 인터페이스
    • 네트워크 통신을 위해 제공하는 소켓 프로그래밍 인터페이스이다.
  • netlink 소켓 인터페이스
    • 표준 소켓 인터페이스를 사용하여 커널과의 양방향 통신을 제공하는 인터페이스이다.

 

 

참고

 

do_fork()

 

프로세스 생성

리눅스 유저 application에서 fork() 함수를 사용할 때 공유 라이브러리인 glibc를 통해 더 이상 fork syscall을 호출하지 않고 clone syscall을 호출한다. 때문에 clone과 fork는 동일하게 사용한다. 리눅스 커널은 backward 호환을 이유로 fork에 대한 syscall을 열어둔 상태이긴 하지만 거의 사용되지 않는다고 보면된다. 그 동안 fork와 vfork를 많이 비교하는 글들이 있지만 리눅스 커널 버전이 향상되면서 논쟁이 되었던 글들이 조금씩 핀트를 벗어나고 있다. 커널이 v4.x에 이르렀으므로 이제 조금 정리를 해보고자 한다.

 

최초 리눅스 구현 시 vfork가 fork에 비해 매우 light한 구동을 보여줬다. vfork로 생성되는 자식 프로세스는 부모 프로세스와 파이프 등을 사용하여 교류를 할 수 없고 단지 부모 프로세스가 자식 프로세스에게 argument만 전달하고 반대로 종료(exit) 결과만 부모 프로세스에 알려줄 수 있다. 이제는 과거지만 그러한 점 때문에 심플하게 프로세스를 구동하고자 할 때에는 vfork를 선호하였었다. 그러나 추후 리눅스가 fork에 COW(Copy On Write) 기능을 사용하면서 fork 역시 light한 동작을 갖게되었다. 이로인해 vfork를 사용해야 할 커다란 이유가 없어졌고, vfork의 사용은 추천되지 않게되었다. 최종적으로 vfork가 fork(clone)와 매우 유사하게 되었는데, 정확히 vfork와 fork(clone)의 차이 점을 구별해 낼 수 없다면 그냥 fork(clone)를 사용하여야 한다.

 

fork vs vfork

  • fork와 달리 vfork는부모 프로세스와 파이프를 통한 교류를 할 수 없다.
  • vfork는 자식 태스크를 생성시 부모 프로세스를 잠시 블럭한다.
  • vfork는 스레드와 같은 동작을 하도록 CLONE_VM을 사용하여 메모리의 사용량을 대폭 줄였다.
    • 부모 프로세스가 사용하는 mm 디스크립터 생성하지 않고 공유한다.
    • 부모 프로세스가 사용하는 페이지 테이블(pgd)을 공유한다.
      • fork는 새 페이지 테이블(pgd)을 사용하여 COW(Copy On Write) 동작으로 필요 시 마다 메모리를 할당하여 사용한다.
    • 부모 프로세스가 사용하는 vm 영역을 복사하지 않고 공유한다.
    • 예) 부모 프로세스가 20M의 메모리를 사용할 때 자식 프로세스가 사용하는 메모리 비교
      • fork: 약 12 ~ 21M
        • 처음 fork를 하였을 때에는 COW 동작에 메모리를 복사하지 않지만 write 동작이 가해지면 메모리를 할당하여 사용한다.
        • 부모 태스크가 사용했었던 vm을 그대로 상속하기 위해 vma 및 페이지 테이블을 새롭게 복사하여 사용한다.
      • vfork: 약 1.5M
        • application에 대해서는 동일한 가상 주소 메모리를 공유하여 사용하므로 자식 프로세스 관리에 필요한 메모리만 조금 추가된다.
  • 참고

 

 

스레드 생성

프로세스보다 가벼운 스레드가 필요한 경우에는 커널 v2.6부터 구현되어 현재까지도 사용되는 NPTL 라이브러리(-lpthread)를 사용한 POSIX thread를 사용한다. pthread_create() 함수를 사용 시 CLONE_VM을 추가한 clone syscall을 사용한다. 이를 통해 light한 메모리 사용과 빠른 스레드 생성을 할 수있다.

 

리눅스 태스크 생성

do_fork()

 

다음 그림은 유저 모드에서 syscall을 통해 프로세스나 스레드를 생성할 때 대응하는 커널 함수와 CLONE 플래그들을 보여준다.

 

다음 그림은 태스크를 생성 시 CLONE 플래그에 따라 각 루틴이 하는 일들을 보여준다.

  • CLONE 플래그에 해당하는 항목이 설정된 경우 함수에서의 동작이 노란 색 항목과 같이 수행된다.

 

kernel/fork.c -1/2-

/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 */
long do_fork(unsigned long clone_flags,
              unsigned long stack_start,
              unsigned long stack_size,
              int __user *parent_tidptr,
              int __user *child_tidptr)
{
        struct task_struct *p;
        int trace = 0;
        long nr;

        /*
         * Determine whether and which event to report to ptracer.  When
         * called from kernel_thread or CLONE_UNTRACED is explicitly
         * requested, no event is reported; otherwise, report if the event
         * for the type of forking is enabled.
         */
        if (!(clone_flags & CLONE_UNTRACED)) {
                if (clone_flags & CLONE_VFORK)
                        trace = PTRACE_EVENT_VFORK; 
                else if ((clone_flags & CSIGNAL) != SIGCHLD)
                        trace = PTRACE_EVENT_CLONE;
                else
                        trace = PTRACE_EVENT_FORK;

                if (likely(!ptrace_event_enabled(current, trace)))
                        trace = 0;
        }

        p = copy_process(clone_flags, stack_start, stack_size,
                         child_tidptr, NULL, trace);

부모 태스크를 복사(fork)한 새 태스크를 스케줄러에서 깨운다. CLONE 플래그 요청에 따라 각각의 리소스에 대해 부모 리소스를 같이 공유하여 사용하거나 별도로 새로 생성된 태스크에 독립적으로 리소스를 사용하도록 배정한다.

  • 코드 라인 23~33에서 CLONE_UNTRACED 커널 옵션을 지정하여 요청한 경우 ptreacer로 이벤트를 리포팅하지 않도록 제한시킨다. 또한
    • kernel_thread() 함수를 통해 만든 커널 스레드는 항상 CLONE_UNTRACED 커널 옵션을 사용한다.
  • 코드 라인 35~36 copy_process() 함수를 호출하여 각 플래그 요청을 기반으로 자원의 공유 여부를 결정하고 부모 태스크를 기반으로 새 태스크를 상속받아 만든다. (clone)

 

kernel/fork.c -2/2-

        /*
         * Do this prior waking up the new thread - the thread pointer
         * might get invalid after that point, if the thread exits quickly.
         */
        if (!IS_ERR(p)) {
                struct completion vfork;
                struct pid *pid;

                trace_sched_process_fork(current, p);

                pid = get_task_pid(p, PIDTYPE_PID);
                nr = pid_vnr(pid);

                if (clone_flags & CLONE_PARENT_SETTID)
                        put_user(nr, parent_tidptr);

                if (clone_flags & CLONE_VFORK) {
                        p->vfork_done = &vfork;
                        init_completion(&vfork);
                        get_task_struct(p);
                }

                wake_up_new_task(p);

                /* forking complete and child started to run, tell ptracer */
                if (unlikely(trace))
                        ptrace_event_pid(trace, pid);

                if (clone_flags & CLONE_VFORK) {
                        if (!wait_for_vfork_done(p, &vfork))
                                ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
                }

                put_pid(pid);
        } else {
                nr = PTR_ERR(p);
        }
        return nr;
}
  • 코드 라인 11~12에서 글로벌 pid 값을 가져온다. 참조 카운터를 1 증가시킨다. 태스크가 소속된 namespace에서 upid 번호를 알아온다.
  • 코드 라인 14~15에서 유저 모드에서 clone syscall을 통해 즉, sys_clone() 함수를 통해 호출된 경우 부모 태스크의 tid에 child 태스크의 upid 번호를 설정한다.
  • 코드 라인 17~21에서 CLONE_VFORK 플래그를 사용하여 요청한 경우 fork 완료 대기를 위해 준비한다.
  • 코드 라인 23에서 생성된 새 태스크를 깨워 동작시킨다.
  • 코드 라인 29~32에서 CLONE_VFORK 플래그를 사용하여 요청한 경우 태스크가 종료될 때 까지 호출한 부모 태스크는 대기한다.
    • 리눅스에서 fork 대신 vfork를 사용하면 부모와 자식간의 race가 발생하지 않도록 부모 태스크는 여기서 잠시 block되어 있다가 자식 task가 fork된 후에야 wakeup한다.
  • 코드 라인 34에서 pid의 참조 카운터를 1 감소시킨다.

 

kernel/fork.c -1/8-

/*
 * This creates a new process as a copy of the old one,
 * but does not actually start it yet.
 *
 * It copies the registers, and all the appropriate
 * parts of the process environment (as per the clone
 * flags). The actual kick-off is left to the caller.
 */
static struct task_struct *copy_process(unsigned long clone_flags,
                                        unsigned long stack_start,
                                        unsigned long stack_size,
                                        int __user *child_tidptr,
                                        struct pid *pid,
                                        int trace)
{
        int retval;
        struct task_struct *p;

        if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
                return ERR_PTR(-EINVAL);

        if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
                return ERR_PTR(-EINVAL);

        /*
         * Thread groups must share signals as well, and detached threads
         * can only be started up within the thread group.
         */
        if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
                return ERR_PTR(-EINVAL);

        /*
         * Shared signal handlers imply shared VM. By way of the above,
         * thread groups also imply shared VM. Blocking this case allows
         * for various simplifications in other code.
         */
        if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
                return ERR_PTR(-EINVAL);

        /*
         * Siblings of global init remain as zombies on exit since they are
         * not reaped by their parent (swapper). To solve this and to avoid
         * multi-rooted process trees, prevent global and container-inits
         * from creating siblings.
         */
        if ((clone_flags & CLONE_PARENT) &&
                                current->signal->flags & SIGNAL_UNKILLABLE)
                return ERR_PTR(-EINVAL);

        /*
         * If the new process will be in a different pid or user namespace
         * do not allow it to share a thread group or signal handlers or
         * parent with the forking task.
         */
        if (clone_flags & CLONE_SIGHAND) {
                if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
                    (task_active_pid_ns(current) !=
                                current->nsproxy->pid_ns_for_children))
                        return ERR_PTR(-EINVAL);
        }

기존 프로세스의 정보들을 사용하여 새로운 프로세스를 생성한다.

  • 코드 라인 19~20에서 새로운 mount namespace로 태스크 생성을 요청했지만 파일 시스템 정보를 공유할 수 없으므로 실패로 함수를 빠져나간다.
  • 코드 라인 22~23에서 새로운 user namespace로 태스크 생성을 요청했지만 파일 시스템 정보를 공유할 수 없으므로 실패로 함수를 빠져나간다.
  • 코드 라인 29~30에서 thread 생성을 요청했지만 시그널 핸들러의 공유가 필요하다. 그렇지 않은 경우 실패로 함수를 빠져나간다.
    • 스레드 그룹끼리는 시그널을 공유해야 한다.
  • 코드 라인 37~38에서 시그널 핸들러를 공유할 때 같은 가상 주소(VM)를 사용해야한다. 그렇지 않은 경우 실패로 함수를 빠져나간다.
  • 코드 라인 46~48에서 부모 태스크에 SIGNAL_UNKILLABLE 시그널 플래그가 있는 경우 부모 태스크 클론을 요청한 경우 실패로 함수를 빠져나간다.
  • 코드 라인 55~60에서 새로운 user namespace 또는 pid namespace로 태스크 생성을 요청한 경우 시그널 핸들러를 공유할 수 없다. 이러한 경우 실패로 함수를 빠져나간다.

 

kernel/fork.c -2/8-

        retval = security_task_create(clone_flags);
        if (retval)
                goto fork_out;

        retval = -ENOMEM;
        p = dup_task_struct(current);
        if (!p)
                goto fork_out;

        ftrace_graph_init_task(p);

        rt_mutex_init_task(p);

#ifdef CONFIG_PROVE_LOCKING
        DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
        DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
        retval = -EAGAIN;
        if (atomic_read(&p->real_cred->user->processes) >=
                        task_rlimit(p, RLIMIT_NPROC)) {
                if (p->real_cred->user != INIT_USER &&
                    !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
                        goto bad_fork_free;
        }
        current->flags &= ~PF_NPROC_EXCEEDED;

        retval = copy_creds(p, clone_flags);
        if (retval < 0)
                goto bad_fork_free;
  • 코드 라인 1~3에서 태스크 생성 이전에 태스크 생성 관련 시큐리티 후크 함수를 호출한다. 만일 실패(0이 아닐 때)시 fork_out 레이블로 이동한다.
  • 코드 라인 5~8에서 태스크를 생성한다.
    • current task 디스크립터(task_struct 및 thread_info 포함)를 복제한다. 이 때 커널 스택도 생성된다.
  • 코드 라인 10에서 ftrace 추적을 위한 초기화를 수행한다.
  • 코드 라인 12에서 priority inheritency 처리 관련 초기화를 수행한다.
  • 코드 라인 18~24에서 최대 프로세스 생성 제한치를 초과하고, 태스크 생성을 루트 유저가 하지 않았으면 bad_fork_free 레이블로 이동한다.
    • 단 시스템 리소스와 시스템 관리자에 대한 capability가 설정된 경우에는 제한을 무시한다.
  • 코드 라인 25~29에서 부모 프로세스의 플래그에서 최대 프로세스 생성 제한 플래그를 클리어하고 인증(credentials)을 복사한다.

 

kernel/fork.c -3/8-

        /*
         * If multiple threads are within copy_process(), then this check
         * triggers too late. This doesn't hurt, the check is only there
         * to stop root fork bombs.
         */
        retval = -EAGAIN;
        if (nr_threads >= max_threads)
                goto bad_fork_cleanup_count;

        if (!try_module_get(task_thread_info(p)->exec_domain->module))
                goto bad_fork_cleanup_count;

        delayacct_tsk_init(p);  /* Must remain after dup_task_struct() */
        p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
        p->flags |= PF_FORKNOEXEC;
        INIT_LIST_HEAD(&p->children);
        INIT_LIST_HEAD(&p->sibling);
        rcu_copy_process(p);
        p->vfork_done = NULL;
        spin_lock_init(&p->alloc_lock);

        init_sigpending(&p->pending);

        p->utime = p->stime = p->gtime = 0;
        p->utimescaled = p->stimescaled = 0;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
        p->prev_cputime.utime = p->prev_cputime.stime = 0;
#endif
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
        seqlock_init(&p->vtime_seqlock);
        p->vtime_snap = 0;
        p->vtime_snap_whence = VTIME_SLEEPING;
#endif

#if defined(SPLIT_RSS_COUNTING)
        memset(&p->rss_stat, 0, sizeof(p->rss_stat));
#endif

        p->default_timer_slack_ns = current->timer_slack_ns;

        task_io_accounting_init(&p->ioac);
        acct_clear_integrals(p);

        posix_cpu_timers_init(p);

        p->start_time = ktime_get_ns();
        p->real_start_time = ktime_get_boot_ns();
        p->io_context = NULL;
        p->audit_context = NULL;
        if (clone_flags & CLONE_THREAD)
                threadgroup_change_begin(current);
        cgroup_fork(p);
  • 코드 라인 6~8에서 스레드 수가 fork_init()함수에서 초기화한 최대 스레드 수(max_threads) 이상인 경우 실패로 bad_fork_cleanup_count 레이블로 이동한다.
    • max_threads 디폴트: mempages / (8 * 커널 스택 사이즈 / 페이지 사이즈)
  • 코드 라인 10~11에서 현재 태스크의 실행 도메인이 디폴트 실행 도메인이 아니면서 모듈 정보가 없는 경우 bad_fork_cleanup_count 레이블로 이동한다.
  • 코드 라인 13에서 delay accounting이 동작 시 초기화를 수행한다.
  • 코드 라인 14~15에서 생성된 태스크의 수퍼 유저 권한 및 워커 스레드 플래그를 제거하고 태스크 생성 후 실행되지 않도록 PF_FORKNOEXEC 플래그를 제거한다.
  • 코드 라인 16~17에서 생성된 태스크의 자식 및 형제 관련한 리스트를 초기화한다.
  • 코드 라인 18에서 생성된 태스크의 rcu 관련 멤버의 초기화를 수행한다.
  • 코드 라인 22에서 시그널 펜딩 리스트를 초기화한다.
  • 코드 라인 24~25 생성된 프로세스의 각종 실행 타임을 0으로 초기화한다.
    • utime: 유저 코드 실행 시간(jiffies)
    • stime: 시스템 코드 실행 시간(jiffies)
  • 코드 라인 26~33 virtual cpu에 대한 time accounting을 초기화한다.
  • 코드 라인 35~37에서 rss 통계 카운터를 초기화한다.
    • MM_FILEPAGES 카운터, MM_ANONPAGES 카운터, MM_SWAPENTS 카운터
  • 코드 라인 39에서 절전을 위해 사용하는 타이머용 slack 나노초를 부모 값을 상속하여 사용한다.
  • 코드 라인 41에서 태스크에 대한 io 통계 카운터를 초기화한다.
    • CONFIG_TASK_XACCT 커널 옵션 사용시
      • 읽은 바이트 수
      • 기록한 바이트 수
      • 읽은 syscall 수
      • 기록한 syscall 수
    • CONFIG_TASK_IO_ACCOUNTING 커널 옵션 사용 시
      • 디스크로 부터 읽은 바이트 수
      • 디스크에 기록한 비이트 수
      • 디스크에 기록 취소한 바이트 수
  • 코드 라인 42에서 태스크에 대한 mm 관련 accounting 카운터 정보를 초기화한다.
  • 코드 라인 44에서 posix cpu 타이머들을 초기화한다.
  • 코드 라인 46~47에서 monotonic 시간(nsec)과 boot based 시간(nsec)을 구해와 대입한다.
  • 코드 라인 48~49에서 io 및 audit 컨택스트를 초기화한다.
  • 코드 라인 50~51에서 스레드 생성 요청 시 스레드 그룹 변경에 대한 락을 건다.
  • 코드 라인 52에서 cgroup 관련 초기화를 수행한다.

 

kernel/fork.c -4/8-

#ifdef CONFIG_NUMA
        p->mempolicy = mpol_dup(p->mempolicy);
        if (IS_ERR(p->mempolicy)) {
                retval = PTR_ERR(p->mempolicy);
                p->mempolicy = NULL;
                goto bad_fork_cleanup_threadgroup_lock;
        }
#endif
#ifdef CONFIG_CPUSETS
        p->cpuset_mem_spread_rotor = NUMA_NO_NODE;
        p->cpuset_slab_spread_rotor = NUMA_NO_NODE;
        seqcount_init(&p->mems_allowed_seq);
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
        p->irq_events = 0;
        p->hardirqs_enabled = 0;
        p->hardirq_enable_ip = 0;
        p->hardirq_enable_event = 0;
        p->hardirq_disable_ip = _THIS_IP_;
        p->hardirq_disable_event = 0;
        p->softirqs_enabled = 1;
        p->softirq_enable_ip = _THIS_IP_;
        p->softirq_enable_event = 0;
        p->softirq_disable_ip = 0;
        p->softirq_disable_event = 0;
        p->hardirq_context = 0;
        p->softirq_context = 0;
#endif
#ifdef CONFIG_LOCKDEP
        p->lockdep_depth = 0; /* no locks held yet */
        p->curr_chain_key = 0;
        p->lockdep_recursion = 0;
#endif

#ifdef CONFIG_DEBUG_MUTEXES
        p->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_BCACHE
        p->sequential_io        = 0;
        p->sequential_io_avg    = 0;
#endif
  • 코드 라인 1~8에서 누마 시스템에서 메모리 정책을 복제한다.
  • 코드 라인 9~13에서 cpuset 관련 멤버들을 초기화한다.
  • 코드 라인 14~28에서 irq 시작과 끝에 대한 trace 정보 및 조작 관련 멤버를 초기화한다.
  • 코드 라인 29~33에서 lockdep 디버깅 관련 멤버를 초기화한다.
  • 코드 라인 35~37에서 뮤텍스 블럭 디버깅 멤버를 초기화한다.
  • 코드 라인 38~41에서 블럭 디바이스를 다른 디바이스의 캐시 대용으로 사용할 때 관련된 멤버들을 초기화한다.

 

kernel/fork.c -5/8-

        /* Perform scheduler related setup. Assign this task to a CPU. */
        retval = sched_fork(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_policy;

        retval = perf_event_init_task(p);
        if (retval)
                goto bad_fork_cleanup_policy;
        retval = audit_alloc(p);
        if (retval)
                goto bad_fork_cleanup_perf;
        /* copy all the process information */
        shm_init_task(p);
        retval = copy_semundo(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_audit;
        retval = copy_files(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_semundo;
        retval = copy_fs(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_files;
        retval = copy_sighand(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_fs;
        retval = copy_signal(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_sighand;
        retval = copy_mm(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_signal;
        retval = copy_namespaces(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_mm;
        retval = copy_io(clone_flags, p);
        if (retval)
                goto bad_fork_cleanup_namespaces;
        retval = copy_thread(clone_flags, stack_start, stack_size, p);
        if (retval)
                goto bad_fork_cleanup_io;

        if (pid != &init_struct_pid) {
                retval = -ENOMEM;
                pid = alloc_pid(p->nsproxy->pid_ns_for_children);
                if (!pid)
                        goto bad_fork_cleanup_io;
        }

        p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
        /*
         * Clear TID on mm_release()?
         */
        p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr : NULL;
#ifdef CONFIG_BLOCK
        p->plug = NULL;
#endif
#ifdef CONFIG_FUTEX
        p->robust_list = NULL;
#ifdef CONFIG_COMPAT
        p->compat_robust_list = NULL;
#endif
        INIT_LIST_HEAD(&p->pi_state_list);
        p->pi_state_cache = NULL;
#endif
  • 코드 라인 1~4에서 태스크의 스케줄러 관련 멤버들을 초기화한다.
    • p->state: TAKS_RUNNING 상태로 바꾼다.
    • p->prio: normal_prio로 설정한다. (pi boost에 의해 priority가 변경되어 있을 수도 있다)
    • p->sched_class: prio를 보고 rt 또는 cfs 스케줄러로 설정한다. (deadline인 경우 에러)
    • thread_info->cpu: cpu 설정
    • p->on_cpu: 0으로 초기화
    • p->pushable_tasks: rt 오버로드용 리스트 초기화
    • p->pusjable_dl_tasks: dl 오버로드용 RB 트리 초기화
    • thread_info->preempt_count: PREEMPT_ENABLE로 초기화
  • 코드 라인 6~8에서 perf 이벤트 관련한 context 들을 모두 초기화한다.
  • 코드 라인 9~11에서 audit 기능이 동작하는 경우 audit context 블럭을 할당한다.
  • 코드 라인 13에서 시스템V IPC용 공유 메모리(shm)에 관련 리스트의 초기화를 수행한다.
  • 코드 라인 14~16에서 부모 태스크에서 사용중인 시스템V IPC용 세마포어들에 대해 공유(CLONE_SYSVSEM)하거나 새로 초기화하여 사용한다.
    • 공유하여 사용하면 참조 카운터는 1을 증가시키고, 그렇지 않은 경우 초기화하여 사용한다.
  • 코드 라인 17~19에서 부모 태스크에서 열고 사용하고 있는 파일 정보를 공유(CLONE_FILES)하거나 복사하여 사용한다.
    • 공유하여 사용하면 참조 카운터는 1을 증가시키고, 그렇지 않은 경우 해당 파일들의 참조 카운터를 1부터 시작한다.
  • 코드 라인 20~22에서 부모 태스크에서 사용하는 루트 파일 시스템을 공유(CLONE_FS)하거나 복사하여 사용한다.
    • 공유하여 사용하면 참조 카운터는 1을 증가시키고, 그렇지 않은 경우 참조 카운터를 1부터 시작한다.
  • 코드 라인 23~25에서 부모 태스크에서 사용하는 시그널 핸들러 정보를 공유(CLONE_SIGHAND)하거나 복사하여 사용한다.
    • 공유하여 사용하면 참조 카운터는 1을 증가시키고, 그렇지 않은 경우 참조 카운터를 1부터 시작한다.
  • 코드 라인 26~28에서 부모 태스크에서 사용하는 시그널 rlimit 정보를 알아와서 시그널 디스크립터를 생성하여 초기화한다. 단 생성되는 태스크가 스레드인 경우에는 아무일도 하지 않고 성공(0)으로 함수를 빠져나간다.
  • 코드 라인 29~31에서 커널 스레드인경우 거의 아무일도 수행하지 않고, pthread, vfork를 통해 진입한 경우 별도의 mm을 만들지 않고 부모의 mm을 공유한다. 유저 프로세스(fork & clone)를 생성한 경우 자신의 vm을 만들되 부모의 mm 정보를 상속받아 사용한다. 부모가 사용하는 vma 정보와 페이지 테이블 정보를 복사하여 사용한다. (COW)
    • COW 방식을 사용하여 실제 사용메모리는 할당하지 않고, 부모 태스크가 사용하던 vma 및 페이지 테이블만 복사하여 자신의 vm을 구성하고, 유저 태스크가 추후 실제 페이지에 수정을 위해 접근하려 할 때 fault 되어 기존 페이지를 새로 할당받은 곳에 복사하여 사용하는 방식을 사용한다.
    • 공유하여 사용하면 참조 카운터는 1을 증가시키고, 그렇지 않은 경우 참조 카운터를 1부터 시작한다.
  • 코드 라인 32~34에서 부모 태스크에서 사용하는 namespace를 사용하거나 공유(CLONE_NEWNS, CLONE_NEWUTS | CLONE_NEWIPC, CLONE_NEWPID, CLONE_NEWNET) 요청이 있는 경우 부모 태스크의 namespace 정보를 사용하여 새로운 namespace를 생성한다.
    • 유저가  CAP_SYS_ADMIN 권한이 없는 경우 -EPERM 에러로 함수를 빠져나간다.
  • 코드 라인 35~37에서 부모 태스크가 사용하는 io context 정보를 공유(CLONE_IO)하거나 새로 생성하여 사용한다.
  • 코드 라인 38~40에서 부모 태스크가 사용하는 스레드 레지스터 정보를 사용하여 초기화한다. 부모 태스크가 사용하는 TLS 정보를 공유(CLONE_SETTLS)할 수도 있다.
  • 코드 라인 42~47에서 인자로 받은 pid 디스크립터가 &init_struct_pid가 아닌 경우 pid 디스크립터를 새로 할당받는다.
    • fork_idle() 함수를 통해 copy_process() 함수를 호출할 때 인자로 &init_struct_pid 디스크립터가 주어진다.

 

kernel/fork.c -6/8-

        /*
         * sigaltstack should be cleared when sharing the same VM
         */
        if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
                p->sas_ss_sp = p->sas_ss_size = 0;

        /*
         * Syscall tracing and stepping should be turned off in the
         * child regardless of CLONE_PTRACE.
         */
        user_disable_single_step(p);
        clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMU
        clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endif
        clear_all_latency_tracing(p);

        /* ok, now we should be set up.. */
        p->pid = pid_nr(pid);
        if (clone_flags & CLONE_THREAD) {
                p->exit_signal = -1;
                p->group_leader = current->group_leader;
                p->tgid = current->tgid;
        } else {
                if (clone_flags & CLONE_PARENT)
                        p->exit_signal = current->group_leader->exit_signal;
                else
                        p->exit_signal = (clone_flags & CSIGNAL);
                p->group_leader = p;
                p->tgid = p->pid;
        }

        p->nr_dirtied = 0;
        p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
        p->dirty_paused_when = 0;

        p->pdeath_signal = 0;
        INIT_LIST_HEAD(&p->thread_group);
        p->task_works = NULL;

        /*
         * Make it visible to the rest of the system, but dont wake it up yet.
         * Need tasklist lock for parent etc handling!
         */
        write_lock_irq(&tasklist_lock);

        /* CLONE_PARENT re-uses the old parent */
        if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
                p->real_parent = current->real_parent;
                p->parent_exec_id = current->parent_exec_id;
        } else {
                p->real_parent = current;
                p->parent_exec_id = current->self_exec_id;
        }
  • 코드 라인 4~5에서 vfork가 아닌 fork 또는 clone을 통해 VM을 공유하는 경우 시그널 보조 스택을 초기화한다.
  • 코드 라인 11에서 gdb 등이 사용하는 ptrace 디버그용 유저 모드 싱글 스텝을 disable한다.
    • arm 및 arm64 아키텍처는 하드웨어 디버거를 지원받아 사용한다.
    • 참고: ptrace
  • 코드 라인 12에서 trace용 syscall 플래그를 제거한다.
  • 코드 라인 13~15에서 emulator용 syscall 플래그를 제거한다.
  • 코드 라인 16에서 latency tracing을 위한 정보를 초기화한다.
  • 코드 라인 20~23에서 스레드를 생성한 경우 이 스레드의 그룹 리더와 스레드 그룹 리더(tgid)는 부모 태스크가 가리키는 그룹리더와 스레드 그룹 리더를 그대로 사용한다.
  • 코드 라인 24~31에서 스레드가 아닌 프로세스를 생성한 경우 그룹 리더와 스레드 그룹 리더는 자신이된다.

 

kernel/fork.c -7/8-

        spin_lock(&current->sighand->siglock);

        /*
         * Copy seccomp details explicitly here, in case they were changed
         * before holding sighand lock.
         */
        copy_seccomp(p);

        /*
         * Process group and session signals need to be delivered to just the
         * parent before the fork or both the parent and the child after the
         * fork. Restart if a signal comes in before we add the new process to
         * it's process group.
         * A fatal signal pending means that current will exit, so the new
         * thread can't slip out of an OOM kill (or normal SIGKILL).
        */
        recalc_sigpending();
        if (signal_pending(current)) {
                spin_unlock(&current->sighand->siglock);
                write_unlock_irq(&tasklist_lock);
                retval = -ERESTARTNOINTR;
                goto bad_fork_free_pid;
        }

        if (likely(p->pid)) {
                ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);

                init_task_pid(p, PIDTYPE_PID, pid);
                if (thread_group_leader(p)) {
                        init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));
                        init_task_pid(p, PIDTYPE_SID, task_session(current));

                        if (is_child_reaper(pid)) {
                                ns_of_pid(pid)->child_reaper = p;
                                p->signal->flags |= SIGNAL_UNKILLABLE;
                        }

                        p->signal->leader_pid = pid;
                        p->signal->tty = tty_kref_get(current->signal->tty);
                        list_add_tail(&p->sibling, &p->real_parent->children);
                        list_add_tail_rcu(&p->tasks, &init_task.tasks);
                        attach_pid(p, PIDTYPE_PGID);
                        attach_pid(p, PIDTYPE_SID);
                        __this_cpu_inc(process_counts);
                } else {
                        current->signal->nr_threads++;
                        atomic_inc(&current->signal->live);
                        atomic_inc(&current->signal->sigcnt);
                        list_add_tail_rcu(&p->thread_group,
                                          &p->group_leader->thread_group);
                        list_add_tail_rcu(&p->thread_node,
                                          &p->signal->thread_head);
                }
                attach_pid(p, PIDTYPE_PID);
                nr_threads++;
        }
  • 코드 라인 7에서 부모 태스크에서 사용하는 secure computing mode를 가져와서 새 태스크에 복사하여 사용한다.
    • 프로세스에 이미 열린 파일 디스크립터에 대해 exit(), sigreturn(), read() 그리고 write()를 제외한 모든 시스템 호출을 할 수 없게 보호한다. 만일 시도시 SIGKILL이 발생한다.
    • 참고: seccomp(2) – Linux manual page – man7.org
  • 코드 라인 17~23에서 부모 태스크의 시그널 상태를 재평가하여 펜딩된 경우 에러로 함수를 빠져나간다. 그렇지 않은 경우 플래그에서 시그널 펜딩 플래그를 제거한다.
  • 코드 라인  25~28에서 높은 확률로 생성된 태스크에 pid 디스크립터가 지정된 경우 이 태스크의 PIDTYPE_PID에 pid 디스크립터를 지정한다.
  • 코드 라인  29~31에서 생성된 태스크가 스레드 그룹의 리더인 경우 이 태스크의 PIDTYPE_PGID에 부모 태스크의 프로세스 그룹 리더의 pid 디스크립터를 지정한다. 또한 PIDTYPE_SID에 부모 태스크의 세션 id의 pid 디스크립터를 지정한다.
  • 코드 라인 33~36에서 이 태스크가 child reaper인 경우 이 pid namespace의 child_reaper에 생성 태스크를 지정한다. 또한 시그널 플래그에 SIGNAL_UNKILLABLE 플래그를 추가한다.
  • 코드 라인 38~39에서 시그널 리더 pid에 이 pid를 지정하고, 시그널에 지정된 tty도 부모 tty를 지정한다.
  • 코드 라인 40~41에서 init_tasks.tasks 리스트에 생성된 스레드 그룹 리더 태스크를 추가하고, 부모 태스크의 자식으로 이 태스크를 추가한다.
  • 코드 라인 42~43에서 생성된 태스크에 지정된 PIDTYPE_PGID 타입과 PIDTYPE_SID에 이 pid를 연결한다.
  • 코드 라인 44에서 전역 per-cpu 변수인 process_counts 카운터를 1 증가시킨다.
  • 코드 라인 45~48에서 생성된 태스크가 스레드인 경우 부모 태스크의 시그널 카운터들을 1씩 증가시킨다.
  • 코드 라인 49~53에서 프로세스 그룹리더의 스레드 그룹 리스트와 시그널에 생성된 스레드를 추가한다.
  • 코드 라인 54~55에서 생성된 태스크에 지정된 PIDTYPE_PID 타입에 이 pid를 연결한다. 그런 후 전역 변수인 스레드 수(nr_threads)를 증가시킨다.

 

kernel/fork.c -8/8-

        total_forks++;
        spin_unlock(&current->sighand->siglock);
        syscall_tracepoint_update(p);
        write_unlock_irq(&tasklist_lock);

        proc_fork_connector(p);
        cgroup_post_fork(p);
        if (clone_flags & CLONE_THREAD)
                threadgroup_change_end(current);
        perf_event_fork(p);

        trace_task_newtask(p, clone_flags);
        uprobe_copy_process(p, clone_flags);

        return p;

bad_fork_free_pid:
        if (pid != &init_struct_pid)
                free_pid(pid);
bad_fork_cleanup_io:
        if (p->io_context)
                exit_io_context(p);
bad_fork_cleanup_namespaces:
        exit_task_namespaces(p);
bad_fork_cleanup_mm:
        if (p->mm)
                mmput(p->mm);
bad_fork_cleanup_signal:
        if (!(clone_flags & CLONE_THREAD))
                free_signal_struct(p->signal);
bad_fork_cleanup_sighand:
        __cleanup_sighand(p->sighand);
bad_fork_cleanup_fs:
        exit_fs(p); /* blocking */
bad_fork_cleanup_files:
        exit_files(p); /* blocking */
bad_fork_cleanup_semundo:
        exit_sem(p);
bad_fork_cleanup_audit:
        audit_free(p);
bad_fork_cleanup_perf:
        perf_event_free_task(p);
bad_fork_cleanup_policy:
#ifdef CONFIG_NUMA
        mpol_put(p->mempolicy);
bad_fork_cleanup_threadgroup_lock:
#endif
        if (clone_flags & CLONE_THREAD)
                threadgroup_change_end(current);
        delayacct_tsk_free(p);
        module_put(task_thread_info(p)->exec_domain->module);
bad_fork_cleanup_count:
        atomic_dec(&p->cred->user->processes);
        exit_creds(p);
bad_fork_free:
        free_task(p);
fork_out:
        return ERR_PTR(retval);
}
  • 코드 라인 1에서 fork된 태스크의 수를 증가시킨다. (전역 변수 total_forks)
  • 코드 라인 3에서 syscall_tracepoint 관련하여 부모 태스크에 설정된 TIF_SYSCALL_TRACEPOINT 플래그 유무를 현재 태스크에 복사하여 설정한다.
  • 코드 라인 6에서 fork가 발생하였으므로 1개 이상의 대기중인 프로세스 이벤트 리스너들에게 넷링크를 통해 전송한다.
  • 코드 라인 7에서 새 태스크 생성 후 cgroup이 처리할 일을 수행한다.
    • cgroup 태스크 리스트에 추가하고 에서 처리할 일을 수행한다.
    • cgroup 서브시스템에 등록된 fork 후크 함수를 호출한다.
  • 코드 라인 8~9에서 스레드 생성 요청(CLONE_THREAD)인 경우 스레드 그룹 변경에 대한 락을 닫는다.
  • 코드 라인 10에서 fork에 대한 perf 이벤트 처리를 수행한다.
  • 코드 라인 13에서 부모 태스크에서 사용하던 Uprobe 기반 이벤트 트레이스에서 사용하는 context들을 vfork로 만들어진 새 유저 태스크에만 복사한다.

 

새 유저 태스크를 위한 mm 공유 또는 복제

copy_mm()

kernel/fork.c

static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
        struct mm_struct *mm, *oldmm;
        int retval;

        tsk->min_flt = tsk->maj_flt = 0;
        tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
        tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif

        tsk->mm = NULL;
        tsk->active_mm = NULL;

        /*
         * Are we cloning a kernel thread?
         *
         * We need to steal a active VM for that..
         */
        oldmm = current->mm;
        if (!oldmm)
                return 0;

        /* initialize the new vmacache entries */
        vmacache_flush(tsk);

        if (clone_flags & CLONE_VM) {
                atomic_inc(&oldmm->mm_users);
                mm = oldmm;
                goto good_mm;
        }

        retval = -ENOMEM;
        mm = dup_mm(tsk);
        if (!mm)
                goto fail_nomem;

good_mm:
        tsk->mm = mm;
        tsk->active_mm = mm;
        return 0;

fail_nomem:
        return retval;
} 

새 태스크를 위해 부모 mm 디스크립터 및 페이지 테이블을 공유 또는 복사해온다. 다음 조건에 따라 동작이 구분된다.

  • kernel_thread() 함수를 통해 커널 스레드의 생성이 요청된 경우
    • 커널 스레드는 vm 정보 구성을 하지 않으므로 아무런 처리를 하지 않는다.
  • pthread_create() 또는 vfork() 함수를 통해 유저 스레드 또는 유저 태스크의 생성이 요청된 경우
    • 부모가 사용하는 mm 디스크립터 정보를 그대로 공유한다.
  • fork() 또는 clone() 함수를 통해 유저 태스크의 생성이 요청된 경우
    • 생성된 태스크용으로 mm 디스크립터와 페이지 테이블을 새롭게 할당한 후 부모가 사용하는 mm 디스크립터와 페이지 테이블을 복사하여 구성한다. (COW)

 

참고:

  • tsk->mm
    • 커널 스레드인 경우 null
    • 유저 태스크(유저 프로세스 및 유저 스레드)인 경우 mm 디스크립터를 가리킨다.
  • tsk->active_mm
    • 커널 스레드인 경우 마지막에 사용했었던 mm을 가리킨다.
      • 최초 init_mm을 제외하곤 항상 마지막 유저 프로세스의 mm 디스크립터를 가리킨다.
    • 유저 프로세스가 사용하는 mm 디스크립터를 가리킨다.
      • 유저 스레드들은 자신이 소속된 유저 프로세스의 mm 디스크립터를 가리킨다.

 

dup_mm()

kernel/fork.c

/*
 * Allocate a new mm structure and copy contents from the
 * mm structure of the passed in task structure.
 */
static struct mm_struct *dup_mm(struct task_struct *tsk)
{
        struct mm_struct *mm, *oldmm = current->mm;
        int err;

        mm = allocate_mm();
        if (!mm)
                goto fail_nomem;

        memcpy(mm, oldmm, sizeof(*mm));

        if (!mm_init(mm, tsk))
                goto fail_nomem;

        dup_mm_exe_file(oldmm, mm);

        err = dup_mmap(mm, oldmm);
        if (err)
                goto free_pt;

        mm->hiwater_rss = get_mm_rss(mm);
        mm->hiwater_vm = mm->total_vm;

        if (mm->binfmt && !try_module_get(mm->binfmt->module))
                goto free_pt;

        return mm;

free_pt:
        /* don't put binfmt in mmput, we haven't got module yet */
        mm->binfmt = NULL;
        mmput(mm);

fail_nomem:
        return NULL;
} 

새 유저 태스크를 위해 mm 디스크립터와 페이지 테이블을 할당한 후, 부모 태스크가 사용하던 mm 정보와 페이지 테이블을 복사한다.

  • 코드 라인 10~12에서 mm 디스크립터용 kmem 캐시를 통해 새 mm 디스크립터를 할당받아온다.
  • 코드 라인 14에서 부모 mm(oldmm) 디스크립터를 할당받은 mm 디스크립터에 모두 복사한다.
    • 부모 태스크가 사용했었던 모든 vm 정보들이 복사된다. (물론 여기에서는 페이지 테이블은 제외)
  • 코드 라인 16~17에서 부모 태스크의 mm 정보를 사용할 필요가 없는 새 태스크용 mm 디스크립터의 멤버를 일부 초기화시킨다. 이 때 함수내에서 유저 태스크용 페이지 테이블을 할당받아 mm->pgd에 대입한다.
  • 코드 라인 19에서 실행 파일 정보를 복제한다.
  • 코드 라인 21~23에서 부모 태스크가 사용하던 vma 정보들과 페이지 테이블에서 매핑된 유저 엔트리들을 복사한다.
    • COW(Copy On Write) 처리를 위해 실제 부모 태스크가 사용하던 페이지(코드, 데이터 등)을 복사하지 않고 vma 정보와 페이지 테이블만 복사한다. 추후 유저 태스크가 활성화되어 부모 태스크가 사용하던 해당 코드 또는 데이터를 공유하여 접근하는데 만일 쓰기 작업이 수행되는 일이 발생할 때에만 해당 페이지를 복사하는 방식으로 실제 메모리를 지연할당한다. -> 태스크 생성이 빨라지고, 실제 물리 메모리 소모가 줄어든다.

 

mm_init()

kernel/fork.c

static struct mm_struct *mm_init(struct mm_struct *mm, struct task_struct *p)
{
        mm->mmap = NULL;
        mm->mm_rb = RB_ROOT;
        mm->vmacache_seqnum = 0;
        atomic_set(&mm->mm_users, 1);
        atomic_set(&mm->mm_count, 1);
        init_rwsem(&mm->mmap_sem);
        INIT_LIST_HEAD(&mm->mmlist);
        mm->core_state = NULL;
        atomic_long_set(&mm->nr_ptes, 0);
        mm_nr_pmds_init(mm);
        mm->map_count = 0;
        mm->locked_vm = 0;
        mm->pinned_vm = 0;
        memset(&mm->rss_stat, 0, sizeof(mm->rss_stat));
        spin_lock_init(&mm->page_table_lock);
        mm_init_cpumask(mm);
        mm_init_aio(mm);
        mm_init_owner(mm, p);
        mmu_notifier_mm_init(mm);
        clear_tlb_flush_pending(mm);
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
        mm->pmd_huge_pte = NULL;
#endif

        if (current->mm) {
                mm->flags = current->mm->flags & MMF_INIT_MASK;
                mm->def_flags = current->mm->def_flags & VM_INIT_DEF_MASK;
        } else {
                mm->flags = default_dump_filter;
                mm->def_flags = 0;
        }

        if (mm_alloc_pgd(mm))
                goto fail_nopgd;

        if (init_new_context(p, mm))
                goto fail_nocontext;

        return mm;

fail_nocontext:
        mm_free_pgd(mm);
fail_nopgd:
        free_mm(mm);
        return NULL;
}

mm 디스크립터를 초기화하고 유저용 pgd 테이블도 할당한다.

  • 코드 라인 1~33에서 mm 디스크립터를 초기화한다.
  • 코드 라인 35~36에서 유저용 pgd 테이블도 할당하고 mm->pgd에 대입한다.
  • 코드 라인 38~39에서 context id를 0으로 초기화한다.

 

유저용 pgd 테이블 할당

arm 커널에서는 하나의 pgd 테이블을 커널 영역과 유저 영역을 같이 공유하여 사용되고, 각 영역을 분리(split)하는 사이즈는 커널 컴파일 옵션(rpi2: CONFIG_VMSPLIT_2G=y)에 따라 다르다. 유저용 pgdb 페이지 테이블을 만들때 arm 아키텍처는 다음과 같은 일을 수행한다.

  • init_mm->pgd 테이블에서 커널 영역에 해당하는 pgd 엔트리들을 내 pgd 테이블에 복사한다.
    • 커널 영역: CONFIG_VMSPLIT_2G + 16M(모듈 영역)
  • 유저 영역에 해당하는 pgd 엔트리들은 null(0)으로 초기화한다.
  • 만일 low exception 벡터를 사용하는 경우에는 low 벡터 주소가 유저 영역에 위치하므로 이를 access 할 수 있도록 low exception 벡터에 대한 pte 엔트리들도 할당하고 준비해야 한다.

 

32bit arm with LPAE 에서는 3개의 페이지 테이블을 구성하는 것이 달라지고, arm64에서는 커널용 페이지 테이블과 유저용 페이지 테이블이 아예 별도로 구성되어 있으므로 간단히 유저용 pgd 페이지 테이블만 할당 한다.

 

mm_alloc_pgd()

kernel/fork.c

static inline int mm_alloc_pgd(struct mm_struct *mm)
{
        mm->pgd = pgd_alloc(mm);
        if (unlikely(!mm->pgd))
                return -ENOMEM;
        return 0;
}

페이지 테이블을 할당한 후 mm->pgd에 지정한다.

  • 사용하는 아키텍처 및 커널 옵션 구성에 따라 페이지 테이블 할당 구현이 약간씩 다르다.

 

pgd_alloc() – for arm

arch/arm/mm/pgd.c

/*
 * need to get a 16k page for level 1
 */
pgd_t *pgd_alloc(struct mm_struct *mm)
{
        pgd_t *new_pgd, *init_pgd;
        pud_t *new_pud, *init_pud;
        pmd_t *new_pmd, *init_pmd;
        pte_t *new_pte, *init_pte;

        new_pgd = __pgd_alloc();
        if (!new_pgd)
                goto no_pgd;

        memset(new_pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t));

        /*
         * Copy over the kernel and IO PGD entries
         */
        init_pgd = pgd_offset_k(0);
        memcpy(new_pgd + USER_PTRS_PER_PGD, init_pgd + USER_PTRS_PER_PGD,
                       (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));

        clean_dcache_area(new_pgd, PTRS_PER_PGD * sizeof(pgd_t));

#ifdef CONFIG_ARM_LPAE
        /*
         * Allocate PMD table for modules and pkmap mappings.
         */ 
        new_pud = pud_alloc(mm, new_pgd + pgd_index(MODULES_VADDR),
                            MODULES_VADDR);
        if (!new_pud)
                goto no_pud;

        new_pmd = pmd_alloc(mm, new_pud, 0);
        if (!new_pmd)
                goto no_pmd;
#endif

arm 아키텍처용 pgd 페이지 테이블을 할당한 후 반환한다.

  • 코드 라인 11~13에서 pgd 페이지 테이블을 할당해온다.
    • 3레벨을 사용하는 LPAE 시스템인 경우에는 4개 엔트리 * 8 바이트 = 32 바이트를 pgd 전용 kmem cache를 통해 할당한다.
    • 2레벨을 사용하는 경우에는 버디 시스템을 통해 4개 페이지 * 4K = 16K 페이지를 할당한다.
  • 코드 라인 15에서 pgd 테이블에서 유저 영역에 해당하는 부분만 0으로 초기화한다.
    • rpi2 예) 2048개의 리눅스 pgd 엔트리에서 하위 유저 영역에 해당하는 부분은 1024(하위 2G 해당)-8(16M 모듈)=1018개의 8바이트 엔트리를 0으로 초기화한다.
  • 코드 라인 20~22에서 커널용 pgd 테이블에서 커널 영역에 해당하는 부분만 새로 할당한 페이지의 커널 영역에 복사한다.
    • 주의: 커널 영역 사이즈는 커널 쪽 vm_split 영역 사이즈와 모듈 영역을 더해야 한다.
  • 코드 라인 24에서 할당받은 pgd 테이블 주소와 사이즈만큼에 해당하는 영역에 대해 data 캐시를 클린한다.
  • 코드 라인 26~38에서 3레벨 페이지 테이블을 구성해야 하는 LPAE 시스템에서는 pud는 pgd를 그대로 사용하여 패스시키고 pmd 테이블도 추가로 할당하여 구성한다.

 

        if (!vectors_high()) {
                /*
                 * On ARM, first page must always be allocated since it
                 * contains the machine vectors. The vectors are always high
                 * with LPAE.
                 */
                new_pud = pud_alloc(mm, new_pgd, 0);
                if (!new_pud)
                        goto no_pud;

                new_pmd = pmd_alloc(mm, new_pud, 0);
                if (!new_pmd)
                        goto no_pmd;

                new_pte = pte_alloc_map(mm, NULL, new_pmd, 0);
                if (!new_pte)
                        goto no_pte;

                init_pud = pud_offset(init_pgd, 0);
                init_pmd = pmd_offset(init_pud, 0);
                init_pte = pte_offset_map(init_pmd, 0);
                set_pte_ext(new_pte + 0, init_pte[0], 0);
                set_pte_ext(new_pte + 1, init_pte[1], 0);
                pte_unmap(init_pte);
                pte_unmap(new_pte);
        }

        return new_pgd;

no_pte:
        pmd_free(mm, new_pmd);
        mm_dec_nr_pmds(mm);
no_pmd:
        pud_free(mm, new_pud);
no_pud:
        __pgd_free(new_pgd);
no_pgd:
        return NULL;
}
  • 코드 라인 1~26에서 low exception 벡터를 사용하는 시스템에서는 벡터가 유저 영역에 위치하므로 이를 위한 별도의 매핑을 추가로 만들어줘야 한다.
  • 코드 라인 28에서 정상적으로 할당받은 pgd 테이블의 시작 주소를 반환한다.

 

pgd_alloc() – for arm64

arch/arm64/mm/pgd.c

pgd_t *pgd_alloc(struct mm_struct *mm)
{
        if (PGD_SIZE == PAGE_SIZE)
                return (pgd_t *)__get_free_page(PGALLOC_GFP);
        else
                return kmem_cache_alloc(pgd_cache, PGALLOC_GFP);
}

arm64에서는 커널용 페이지 테이블과 유저용 페이지 테이블이 아예 별도로 구성되어 있으므로 간단히 유저용 pgd 페이지 테이블만 할당 한다.

 

참고