'2016/07'에 해당되는 글 1건

  1. 2016.07.02 [Linux/Kernel] Kernel 4.6.3 new read() implementation

- Kernel 4.6.3 read() syscall internal implementation -


이번에 Kernel 버전이 4.0으로 바뀌면서 read syscall의 커널 내부 구현이 상당수 바뀌었다. 기존에 존재하던 do_sync_read함수가 없어지고 아예 new_sync_read만이 남았다던가... 그런 식으로 말이다. 물론 3.0부터 커널 소스의 개편이 예고되었지만, 막상 바뀌고 나니까 당혹스러웠다고나 할까. 본인도 zero copy에 대해 정리하면서 read의 구현을 살펴 보고 나서야 알아차린 사실이니까 말이다. 그래서 이거 뭐가 중요하냐고? 딱히 중요한 건 아니다. 사실 이번 4.0 커널에서 새로이 수정된 내용들은 3.0 커널 때부터 천천히 바뀌어 온 내용이니까. 하지만, 기왕 바뀐 김에 이렇게 짚고 넘어가는 것도 나쁘지 않을 것 같다고나 할까나... 아무튼 간에, 비교 대상은 Kernel 4.6.3.과 Kernel 3.18이다.


가장 먼저 read() syscall을 호출하게 되면 내부적으로 Syscall Table을 참고하여 해당 syscall의 커널 래퍼 루틴을 찾게 된다. 커널 소스에서는 이런 syscall 래퍼 루틴을 SYSCALL_DEFINE(n) 매크로의 형태로 정의하게 된다. 아무튼가 read syscall의 흐름을 따라가보도록 하자.


1
2
3
4
5
6
7
8
9
10
11
12
13
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
    struct fd f = fdget_pos(fd);
    ssize_t ret = -EBADF;
    if (f.file) {
        loff_t pos = file_pos_read(f.file);
        ret = vfs_read(f.file, buf, count, &pos);
        if (ret >= 0)
            file_pos_write(f.file, pos);
        fdput_pos(f);
    }
    return ret;
}
cs


위 소스코드는 SYSCALL_DEFINE3(read...의 코드이다. 다행히 이 부분에 있어서는 크게 바뀐 점이 없다. fget_pos함수를 통해 fd구조체를 얻어오는 것부터 시작해서 file_pos_read를 통해 File Pointer를 얻어오는 것도 그렇고, 파일을 읽기 위해 vfs_read를 호출하는 것도 그렇고, 결국 'File 구조체를 넘겨 vfs_read를 호출한다'는 기본 개념에서 크게 달라진 점은 없는 것 같다. 그럼 계속 분석해 나가볼까.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    ssize_t ret;
    if (!(file->f_mode & FMODE_READ))
        return -EBADF;
    if (!(file->f_mode & FMODE_CAN_READ))
        return -EINVAL;
    if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
        return -EFAULT;
    ret = rw_verify_area(READ, file, pos, count);
    if (ret >= 0) {
        count = ret;
        ret = __vfs_read(file, buf, count, pos);
        if (ret > 0) {
            fsnotify_access(file);
            add_rchar(current, ret);
        }
        inc_syscr(current);
    }
    return ret;
}
cs


그런데 여기도 크게 달라진 점은 없는 듯하다. File을 읽을 수 있는지 검사하는 4~7번 라인과 User mode buffer가 유효 메모리 범위 안에 존재하는지 확인하는 access_ok함수도 그대로 존재한다. 동일하게, 10번 라인에 현재 파일에 락(mandatory lock)이 걸려있는지 확인하는 rw_verify_area 또한 그대로 존재한다. 언뜻 보이는 차이점이라고 하자면은... 중간에 __vfs_read라는 함수가 추가된 점?


1
2
3
4
5
6
7
8
9
10
ssize_t __vfs_read(struct file *file, char __user *buf, size_t count,
           loff_t *pos)
{
    if (file->f_op->read)
        return file->f_op->read(file, buf, count, pos);
    else if (file->f_op->read_iter)
        return new_sync_read(file, buf, count, pos);
    else
        return -EINVAL;
}
cs


__vfs_read의 구현을 살펴보았다. File System의 file_operations구조체에 등록된 read함수가 존재하는 경우 해당 함수를 가장 먼저 호출하고, 차순으로 file_operations에  read_iter함수가 정의되어 있는 경우에 new_sync_read함수를 호출하게 된다. 이도 저도 아닌, 즉 파일을 읽어 들일 수 있는 함수가 존재하지 않는 경우에는 -EINVAL 에러 코드를 리턴하게 된다.

그런데... 어디서 많이 본 구조 같은데... 아무래도 기존의 vfs_read함수 자체에 들어있던 내용을 별도의 함수로서 빼낸 것 같다. 좀 더 확실한 비교를 위해 Diffing툴을 이용해서 차이점을 확인해보도록 하겠다.



...역시나라고 해야하나. 3.0 커널에서 vfs_read함수에 존재하던 부분을 __vfs_read라는 별도의 함수로 빼낸 듯하다. 그런데 뭔가 일부 내용이 바뀐 듯 하다. 잠시 해당 부분 코드만 빼내서 비교를 해보도록 하겠다.



바뀌었다. read함수를 최우선적으로 호출하는 점에 있어서는 변하지 않았지만, 기존에 aio_read와 관련되어있던 do_sync_read함수 부분이 아예 사라진 것을 볼 수 있다. 또한 new_sync_read함수가 aio_read를 대체하고 있는 것을 볼 수 있다. 정리하자면, 과거에는 aio_read함수가 등록된 File System가 존재하면 do_sync_read 함수를 new_sync_read함수보다 먼저 호출하였다. 즉, 과거에는 최신 명세보다 기존의 aio_read의 호환성을 좀 더 신경썼다는 것. 하지만 aio_read가 존재하는지 확인하는 부분 자체가 없어지고 new_sync_read만을 남겨두었다는 뜻은...



아무래도, do_sync_read함수 자체를 아예 new_sync_read로 대체하기로 한 것이 아닐까. 그 증거라고 할까 do_sync_read의 구현 자체가 커널 소스 상에서 사라졌다. 비록 하위 호환성을 포기하는 한이 있더라도 4.0 커널에서는 new_sync_read만을 사용하기로 생각한 것 같다.

물론 이렇게 되었다고 하더라도 File System에서 지원하는 read함수를 우선 호출한다는 점에서는 변하는 것이 없지만, 가만히 생각해보면 커널 4.0부터의 Ext4의 file_operations구조체에는 .read가 따로 정의되어 있지 않다. 여기서 잠시 Kernel 4.6.3 Ext4 File System의 file_operations구조체의 구현을 확인해보도록 하자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const struct file_operations ext4_file_operations = {
    .llseek        = ext4_llseek,
    .read_iter    = generic_file_read_iter,
    .write_iter    = ext4_file_write_iter,
    .unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl    = ext4_compat_ioctl,
#endif
    .mmap        = ext4_file_mmap,
    .open        = ext4_file_open,
    .release    = ext4_release_file,
    .fsync        = ext4_sync_file,
    .splice_read    = generic_file_splice_read,
    .splice_write    = iter_file_splice_write,
    .fallocate    = ext4_fallocate,
};
cs


보다시피, 별도의 .read멤버가 정의되어 있지 않다. 여기서 정의되어 있는 것은 .read_iter뿐이다. 즉, 4,0 커널의 vfs_read에서 Ext4 File System은 __vfs_read함수 else if문의 조건을 충족하여 new_sync_read함수를 호출하게 된다. 그럼 과거에 .read멤버가 존재했을 때는 어땠을까?



물론 .read멤버가 존재했던 Kernel 3.18에서도 ext4 file_operations의 .read에는 new_sync_read가 들어가 있었다. 이때부터 이미 new_sync_read가 쓰이고는 있었다는 것. 어쩌면 '__vfs_read에서 .read호출해서 new_sync_read가 호출되던, .read_iter가 else if문에 걸려서 new_sync_read가 호출되던 둘 다 결국에는 똑같은 상황이잖아'라는 생각에 지웠을지도 모르겠다.

아무튼 간에 이쯤 되면 Critical하지는 않지만, 무언가 세세한 부분에서 많이 바뀌었다는 것을 느낄 수 있을 것이다. 그럼, 계속해서 사실상 do_sync_read를 대신해 표준으로 바뀐 new_sync_read함수의 구현을 따라 좀 더 깊은 곳으로 뛰어들어 보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
static ssize_t new_sync_read(struct file *filp, char __user *buf, size_t len, loff_t *ppos)
{
    struct iovec iov = { .iov_base = buf, .iov_len = len };
    struct kiocb kiocb;
    struct iov_iter iter;
    ssize_t ret;
    init_sync_kiocb(&kiocb, filp);
    kiocb.ki_pos = *ppos;
    iov_iter_init(&iter, READ, &iov, 1, len);
    ret = filp->f_op->read_iter(&kiocb, &iter);
    BUG_ON(ret == -EIOCBQUEUED);
    *ppos = kiocb.ki_pos;
    return ret;
}
cs


글쎄다. 이번에도 크게 바뀐 부분은 없는 것 같다. read_iter를 호출하는 건 메인 루틴이니까 그렇다고 쳐도, init_sync_kiocb를 호출해서 kiocb구조체를 초기화하는 것도, iov_iter_init을 이용해서 io vector iterator를 초기화하는 것도 달라진 부분이 없다. 좀 더 자세히 확인하기 위해 Diffing툴을 이용해보자.



예상대로 크게 바뀐 부분은 없다. 기존에 -EIOCBQUEUED를 검사하여 예외 처리하는 루틴이 BUG_ON 매크로로 바뀐 것 뿐이다. 물론 ki_nbytes멤버를 초기화하는 부분이 사라졌지만, 전체 흐름에 있어 크게 상관 없는 부분이라 넘어가도록 하겠다.

그럼 전제적인 코드가 아니라 내부 함수까지 세세히 한번 비교해볼까. init_sync_kiocb의 구현을 살펴보도록 하자.


1
2
3
4
5
6
7
static inline void init_sync_kiocb(struct kiocb *kiocb, struct file *filp)
{
        *kiocb = (struct kiocb) {
                .ki_filp = filp,
                .ki_flags = iocb_flags(filp),
        };
}
cs


여기서는 Kernel 3.16에 비해 달라진 부분이 바로 보인다. 명확한 비교를 위해 Kernel 3.16의 소스 코드도 보도록 하자.


1
2
3
4
5
6
7
8
static inline void init_sync_kiocb(struct kiocb *kiocb, struct file *filp)
{
        *kiocb = (struct kiocb) {
                        .ki_ctx = NULL,
                        .ki_filp = filp,
                        .ki_obj.tsk = current,
                };
}
cs


보다시피, .ki_filp를 초기화 하는 부분을 제외하고는 모두 바뀌었는데, .ki_ctx의 초기화 부분이 없어졌을 뿐더러, .ki_obj.tsk의 초기화 부분은 어디 가고 .ki_flags라는 멤버를 iocb_flag라는 함수의 리턴 값으로 초기화 시키고 있다.


혹시나 해서 kiocb구조체의 명세를 살펴보았는데 상당수가 바뀌어 있는 것을 발견할 수 있었다. 특히 aio_read 관련 멤버들이 사라진 것이 눈에 띈다. 새로이 추가된 멤버로는 void* private와 int형 ki_flags가 있는데, ki_flags라... 뭔가 느낌이 오지 않는가? 위에서 본 새로운 init_sync_kiocb함수에서 해당 멤버를 iocb_flags라는 함수로 초기화 했는데, iocb_flags 함수의 구현을 보자.


1
2
3
4
5
6
7
8
9
static inline int iocb_flags(struct file *file)
{
        int res = 0;
        if (file->f_flags & O_APPEND)
                res |= IOCB_APPEND;
        if (io_is_direct(file))
                res |= IOCB_DIRECT;
        return res;
}
cs


보자마자 바로 어떤 역할을 하는지 앟 수 있다. inline함수로서, file구조체의 f_flags멤버를 검사하는 루틴이다. 두 번째 if문을 보면 io_is_direct라는 함수가 보이고, 해당 함수의 리턴 값이 true일 경우, IOCB_DIRECT를 set하는 것을 볼 수 있다. 여기서, io_is_direct함수 또한 inline함수이며,


1
return (filp->f_flags & O_DIRECT) || IS_DAX(filp->f_mapping->host);
cs


위와 같이 O_DIRECT플래그가 켜져있는지, 혹은 read 대상 파일이 DAX파일인지를 검사하게 된다. 즉, 새로운 init_sync_kiocb함수에서는 File Pointer를 초기화하고, iocb_flags함수를 이용하여 각 파일 플래그에 맞게 ki_flags멤버를 설정해주게 된다.

자, 새로운 init_sync_kiocb함수를 최종적으로 정리해볼까. 기존에 존재하던 kiocb구조체의 멤버들을 read에 필수적인 멤버들만을 남기고 가지치기 하였고, 그에 따라 init_sync_kiocb함수는 kiocb구조체에 File Pointer와 File Flags만을 저장하도록 경량화(이 표현이 맞는건지는 잘 모르겠다) 되었다는 것을 알 수 있다.


init_sync_kiocb다음으로 new_sync_read에서 호출되는 함수는 iov_iter_init함수다. 이번에는 iov_iter_init함수가 어떻게 바뀌었는지 확인하자.



이번에도 큰 흐름에서 벗어난 부분은 없다. 다만, 과거에는 segment_eq로 확인해서 포함되어있는 segment가 같을 경우 direction에 ITER_KVEC만을 set 해줬었는데, 커널 4.0부터는 kvec구조체가 iov_iter구조체에 멤버로 들어온 듯 하며, kvec구조체 또한 IO Vector인 iov를 형 변환시켜 초기화 해주고 있다.

여기서 전혀 다른 구조체인 kvec과 iovec을 어떻게 형 변환 시키냐고 생각할 수 도 있을텐데, 아실 분은 아시겠지만 사실 kvec은


1
2
3
4
struct kvec {
        void *iov_base;
        size_t iov_len;
};
cs


1
2
3
4
5
struct iovec
{
        void __user *iov_base;
        __kernel_size_t iov_len;
};
cs


보다시피 처음부터 iovec과의 호환이 가능하도록 설계되어 있기 때문에 큰 문제가 없다. 뭐, 결론적으로 iov_iter_init은 iov_iter구조체에 새로이 들어온 kvec멤버의 초기화 부분이 추가되었다는 점 빼고는 다른 부분이 없는 듯 하다.


그럼 다시 거슬러 올라가 new_sync_read를 보자. 이제는 더 비교할 부분도 없다. 또 다시 더 깊게 들어가보자. 각 File System의 file_operations에서 read_iter를 호출하게 되는데, 일반적인 경우 read_iter는 generic_file_read_iter함수로서 정의되어 있다. generic_file_read_iter를 뜯어보도록 하겠다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
ssize_t
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
        struct file *file = iocb->ki_filp;
        ssize_t retval = 0;
        loff_t *ppos = &iocb->ki_pos;
        loff_t pos = *ppos;
        size_t count = iov_iter_count(iter);
 
        if (!count)
                goto out; /* skip atime */
 
        if (iocb->ki_flags & IOCB_DIRECT) {
                struct address_space *mapping = file->f_mapping;
                struct inode *inode = mapping->host;
                loff_t size;
 
                size = i_size_read(inode);
                retval = filemap_write_and_wait_range(mapping, pos,
                                        pos + count - 1);
                if (!retval) {
                        struct iov_iter data = *iter;
                        retval = mapping->a_ops->direct_IO(iocb, &data, pos);
                }
 
                if (retval > 0) {
                        *ppos = pos + retval;
                        iov_iter_advance(iter, retval);
                }
 
                /*
                 * Btrfs can have a short DIO read if we encounter
                 * compressed extents, so if there was an error, or if
                 * we've already read everything we wanted to, or if
                 * there was a short read because we hit EOF, go ahead
                 * and return.  Otherwise fallthrough to buffered io for
                 * the rest of the read.  Buffered reads will not work for
                 * DAX files, so don't bother trying.
                 */
                if (retval < 0 || !iov_iter_count(iter) || *ppos >= size ||
                    IS_DAX(inode)) {
                        file_accessed(file);
                        goto out;
                }
        }
 
        retval = do_generic_file_read(file, ppos, iter, retval);
out:
        return retval;
}
EXPORT_SYMBOL(generic_file_read_iter);
cs



달라진 부분만 보자. 먼저 count가 0일 경우 모든 루틴을 스킵하는 부분이 추가되었고, Diffing 결과 1848번 라인을 보면 기존 1699번 라인에서 직접 f_flags로 Direct IO여부를 결정하던 것이 위에서 언급했던 ki_flags의 도입으로 해당 멤버가 IOCB_DIRECT로 set되어 있는지 검사하는 식으로 바뀌었다.

또한, Diffing 결과 1712번 라인에 direct_IO함수에서 rw 매개변수가 사라졌다. 여기서 잠시 address_space_operations구조체를 확인보면,



실제로 direct_IO의 Prototype에서 첫 번째 인자인 int형이 없어진 것을 볼 수 있다. 좀 더 확실히 하기 위해, direct_IO의 File System Wrapper함수들 중 가장 대표적인 예로서 ext4의 ext4_direct_IO를 비교해보았다.


Diffing 결과 24번 라인에서 29번 라인을 살펴보면 이제 매개변수로서 rw를 판단하는 것이 아닌, direct_IO함수 자체에서 iov_iter_rw를 호출해서 read/write 중 무슨 작업을 수행해야하는지 판단한다는 것을 알 수 있다. iov_iter_rw는 매크로 함수로서 다음과 같은 형태를 띄고 있다.


1
#define iov_iter_rw(i) ((0 ? (struct iov_iter *)0 : (i))->type & RW_MASK)
cs


쉽게 말하자면, iov_iter의 type에 RW_MASK를 AND연산해 R/W판단을 한다고 보면 될 듯하다. 정리하자면, 자체적으로 iov_iter_rw를 이용해 R/W판단을 수행하기에 더 이상 rw 매개변수가 필요 없다는 선택인 듯 하다. 이 이상으로 Ext4의 direct_IO를 세밀하게 분석하면 read의 분석이 아닌 Ext4의 분석이 되므로 다시 generic_file_read_iter로 돌아오도록 하자.


Direct IO에서 바뀐점을 제외하고 나면, 이제는 Diffing 결과 1875번 라인에서 DAX파일에서의 Buffered Read에 대한 패치가 적용된 정도 밖에 보이지 않는다. 슬슬 read의 내부 구현 분석도 끝이 보이는 듯 하다. 마지막으로 사실상 파일을 직접적으로 읽는 함수인 do_generic_file_read함수만을 남겨뒀는데, 안타깝게도 do_generic_file_read함수의 내부 구현을 모두 분석하기에는 너무 양이 방대해지는 관계로 이후 별도의 포스팅으로 빼내어 설명하도록 하겠다. 따라서 본 포스팅은 커널 4.0에 들어서 달라진 do_generic_file_read함수의 변경 부분만을 설명하고 끝내도록 하겠다



첫 번째 변경 부분이다. PAGE_CACHE_MASK가 PAGE_MASK로 바뀌고, PAGE_CACHE_SHIFT가 PAGE_SHIFT로 바뀌는 등, 매크로의 이름에서 CACHE가 빠졌다. 그럼 이로 인해 생기는 영향은 뭘까?


1
2
3
4
#define PAGE_CACHE_SHIFT        PAGE_SHIFT
#define PAGE_CACHE_SIZE         PAGE_SIZE
#define PAGE_CACHE_MASK         PAGE_MASK
#define PAGE_CACHE_ALIGN(addr)  (((addr)+PAGE_CACHE_SIZE-1)&PAGE_CACHE_MASK)
cs


간단하게 말하자면 '그런거 없다'. 애초에 PAGE_CACHE_SHIFT와 같이 CACHE계열 매크로들은 PAGE_SHIFT의 다른 이름에 불과했던 것이다. 루틴이 달라진 점은 없고, 단순히 불필요한 매크로의 정리 그 이상 그 이하도 아니다.



두 번째 변경 부분이다. 무언가 새로 생긴 부분이 있다. wait_on_page_locked_killable라는 함수가 중간에 추가되었고, page_ok로 분기하는 if문이 추가되었다.

일단 먼저 해당 부분이 추가된 스코프를 보자. if (!PageUptodate(page))라는 조건문에 의해 형성된 스코프이다. 해당 조건문의 PageUptodate함수는 인자로 넘겨진 페이지에 대해 test_bit함수로 플래그 검사를 수행하며, PG_uptodate플래그(현재 페이지 캐시에 저장된 파일의 내용이 Valid할 경우 set, 즉 unset시 페이지 캐시의 갱신이 필요)가 set되어 있는지 검사하게 된다. 따라서 이 스코프는 현재 페이지 캐시에 올라와 있는 파일의 내용이 Valid하지 않았을 때 수행하는 루틴이 포함된 스코프이다.

이때, wait_on_page_locked_killable함수를 수행한 뒤, PageUptodate함수로 다시 PG_uptodate를 검사해 set되어 있으면 디스크 접근 루틴을 생략하고 바로 page_ok로 분기하게 된다.

wait_on_page_locked_killable은 페이지가 Lock 되어 있는 상태일 시, 내부적으로 __wait_on_bit함수를 호출해 페이지가 Unlock(PG_locked가 해제) 될 때 까지 Waiting하게 된다. wait_on_page_locked_killable함수를 페이지가 Unlock 되기까지 기다린 후에 다시 한번 PageUptodate를 통해 Valid Check를 하게 된다.

과거의 경우, 페이지의 내용이 Valid하지 않을 경우 is_partially_uptodate등의 함수를 호출해 최소한의 추가 검증만을 수행 하고 나서, 바로 page_not_up_to_date 혹은 page_not_up_to_date_locked와 같이 페이지를 새로 읽어오기 위한 레이블로 분기했었다.

하지만, 단순 PG_uptodate플래그만 검사할 경우, 페이지가 사용 중이거나, Truncation 과정 중에 있어 Lock이 걸린 상태, 기타 Race-Condition상태 등의 일부 상황에서 페이지 내부 데이터가 Valid하더라도 페이지가 non-Valid로 판단되는 경우가 있었다. 커널 4.0에서는 해당 상황에 대해 대비하기 위해 페이지에 Lock이 걸려있을 경우에는 wait_on_page_locked_killable을 통해 Unlock이 될 때까지 기다린 후에 한번 더 PageUpdate를 수행하게 된다.

물론, Performance 부분에 있어서는 Waiting을 통해 소비되는 시간이 손해라고 생각 될 수도 있지만, wait_on_page_locked_killable이 도입된 이유는 불필요한 Serialisation을 최소화 하기 위해서였으니까 단순 Performance로만 판단하기에는 무리가 따른다. 또, wait_on_page_locked_killable의 경우 기존의 wait_on_page_locked함수와 다르게 Killable하다는 점에 있어서 특정 상황에 있어서 융통성을 가질 수 있다는 점이 다르다.



세 번째로 바뀐 부분, 기존의 page_cache_release함수가 put_page함수로 바뀌었다. 하지만 이 경우에도 처음 CACHE계열 매크로의 제거와 같은 맥락이다.


1
#define page_cache_release(page)        put_page(page)
cs


page_cache_release 또한 매크로 함수로서, 사실상 put_page와 동일했다. 즉, 이 부분도 기능적으로 바뀐 내용은 없다는 것.



마지막 바뀐 부분이다. add_to_page_cache_lru에 들어가는 인자 중, 제일 마지막 GFP_KERNEL부분이 mapping_gfp_constraint(mapping, GFP_KERNEL)로 바뀌었다.


1
2
3
4
5
6
7
8
9
10
11
static inline gfp_t mapping_gfp_mask(struct address_space * mapping)
{
        return (__force gfp_t)mapping->flags & __GFP_BITS_MASK;
}
 
/* Restricts the given gfp_mask to what the mapping allows. */
static inline gfp_t mapping_gfp_constraint(struct address_space *mapping,
                gfp_t gfp_mask)
{
        return mapping_gfp_mask(mapping) & gfp_mask;
}
cs


mapping_gfp_constraint의 구현을 보면 내부에서 mapping_gfp_mask를 mapping을 넘겨주면서 한번 더 호출하게 되며, mapping_gfp_mask는 mapping의 flags멤버에 __GFP_BITS_MASK를 AND연산하여 현재 페이지의 GFP(Get Free Page)플래그를 구하게 된다. 그리고 다시 mapping_gfp_constraint에서 전달받은 GFP_KERNEL과 AND연산을 하여 최종적인 GFP 플래그를 구하게 된다. 과거 add_to_page_cache_lru에서는 현재 페이지와 관계 없이 무조건 인자로서 넘어오는 GFP플래그를 적용시켰는데, 커널 4.0에서는 현재 페이지의 flags를 검사해서 flags에 명시되어 있는, 즉 현재 가능한 GFP플래그만을 넘기도록 강제한 것이라 보면 편할 듯 하다. 아, 그리고 'gfp_t라는 형은 뭐냐'라는 의문이 들 수도 있을텐데...


1
typedef unsigned __bitwise__ gfp_t;
cs


별거 아니고, 사실 Plain Integer다.


뭐, 아무튼간에 이렇게 해서 길다면 길고, 짧다면 짧은 커널 4.0의 read 분석이 끝났다. 물론 do_generic_file_read함수를 전부 분석하지 못한게 본인도 아쉽긴 한데... 그래도 어차피 크게 바뀐 부분도 없을 뿐더러, 나중에 별도의 포스팅에서 자세히 분석할 예정이다. 만일 포스팅 내용중에 틀린 점이나 애매하게 설명된 점이 있으면 지적해주면 바로 정정하겠다.







Posted by RevDev
,