[Linux Kernel] Pipe

dandb3·2024년 9월 19일
0

linux kernel

목록 보기
19/21

sys_pipe()

pipe를 만드는 syscall이다.
둘 다 공통적으로 do_pipe2()를 호출한다.
두 syscall의 차이점은 flag를 인자로 주는지에 대한 여부 차이이다.

do_pipe2()

__do_pipe_flags() 함수를 통해 파이프를 생성한다.
그 후, copy_to_user()를 통해 생성된 새로운 fd값을 user에게 되돌려주고, fd_install()을 통해 새로 할당받은 fd값과 새로 생성된 file끼리 매칭시켜준다.

__do_pipe_flags()

create_pipe_files()를 통해 새로운 file을 생성한다.
get_unused_fd_flags()를 통해 2개의 새로운 fd값을 할당받는다.

create_pipe_files()

get_pipe_inode()를 통해 inode 구조체를 초기화한다.

alloc_file_pseudo()를 통해 pipe의 write-end file 구조체를 생성한다.
alloc_file_clone()을 통해 앞서 만들었던 write-end file 구조체를 clone하는데, 권한은 O_RDONLY로 설정해준다. 이렇게 만들어지는 구조체는 pipe의 read-end에 해당한다.

이 때, 생성된 두 file의 경우 f_ops 필드 값을 pipefifo_fops로 설정하게 된다.

pipefifo_fops

위와 같은 전역 변수에 해당한다.
각 멤버에 대해서는 뒤에 자세히 살펴본다.

get_pipe_inode()

alloc_pipe_info()를 통해 새로운 struct pipe_inode_info를 할당받는다.
나머지는 멤버변수 초기화에 해당한다.

struct pipe_inode_info

설명이 워낙 잘 써져 있어서 위 사진으로 대체한다.
pipe의 경우 ring-buffer로 관리된다.
내부 변수들을 보면, bufs 변수를 통해 실제 ring-buffer가 저장되고,
head, tail, ring_size 등의 변수들을 통해 ring-buffer를 관리하는 것을 확인할 수 있다.

alloc_pipe_info()

pipe의 핵심 구조체들을 할당하는 부분이다.
struct pipe_inode_info 구조체를 새로 할당한 후, struct pipe_buffer 구조체를 할당하게 된다.
여기서 눈여겨 볼 점은 할당 시 kzalloc()을 통해 할당받고, GFP_KERNEL_ACCOUNT 플래그를 통해 kmalloc-cg-xx의 slab cache를 (운영체제가 제공하는 경우) 사용하게 된다.

struct pipe_buffer

각 멤버변수에 대한 설명은 사진으로 대체한다.

기본적으로 page 구조체 포인터를 포함하고 있으므로, 하나의 struct pipe_buffer는 4KB만큼의 데이터를 저장할 수 있다.

alloc_pipe_info()를 다시 살펴보았을 때, PIPE_DEF_BUFFERS의 값은 16으로, struct pipe_buffer를 할당할 때 sizeof(struct pipe_buffer) * PIPE_DEF_BUFFERS = 0x280, 즉 0x400의 slab-cache에서 할당받게 된다.

물론 fcntl(pipefd, F_SETPIPE_SZ, size) syscall을 통해 pipe의 사이즈를 변경할 수도 있다. 이 때, pipefd는 read-end, write-end 둘 다 상관이 없고, size는 0x1000의 배수여야 한다. (몇 배수인지에 따라 할당되는 pipe_buffer의 수가 달라진다.)
사이즈를 변경하는데 있어서 앞서 struct pipe_inode_inforing_size 변수값이 pipe의 사이즈를 의미하는데, 항상 2의 제곱수로 rounded 된다.

플래그 값 중 PIPE_BUF_FLAG_CAN_MERGE는 기억해 두도록 하자. 나중에 쓸 예정.

pipefifo_fops

이제 pipefifo_fops에 대해 자세히 알아보자.

생성된 pipe에 대한 여러 operations가 저장되어 있다.
몇 가지 operations에 대해 살펴보자.

pipe_write()

함수가 길어서 부분별로 살펴볼 것이다.

만약 pipe가 비어있지 않고, write할 데이터도 PAGE_SIZE의 배수가 아니라면 마지막 buffer에 내용을 이어붙인다.

여기서 mask 값을 pipe->ring_size - 1로 한 이유는 ring_size 자체가 2의 제곱수이기 때문에 (앞에서 설명함) 1만 빼면 마스크 값으로 쓸 수 있는 것이다.
그래서 이전 buffer의 주소를 구할 때 &pipe->bufs[(head - 1) & mask]의 꼴로 쓸 수 있는 것임.

만약 이전 buffer의 flagPIPE_BUF_FLAG_CAN_MERGE가 set 되어 있었고, charsoffset의 합이 PAGE_SIZE보다 작거나 같다면 copy_page_from_iter()를 통해서 buffer에 내용을 이어붙이게 된다.

pipe가 꽉 차지 않았다면, pipe에 데이터를 써 가는 과정을 시작한다.
pipe->tmp_page를 먼저 사용하게 되는데, 만약에 없다면 새로 page를 할당받은 후에 pipe->tmp_page에 저장하게 된다.

pipe->head의 값을 1만큼 미리 더하고, buffer의 값을 초기화한다.
만약 write하려고 하는 내용이 packetized 되었다면 (하나의 패킷의 형태로 독립적으로 존재, 뒤에 다른 데이터가 붙을 수 없음), flag값을 PIPE_BUF_FLAG_PACKET으로 설정한다.
그렇지 않다면 flag값을 PIPE_BUF_FLAG_CAN_MERGE로 설정하여 이후의 pipe_write() 시 내용을 이어붙일 수 있게 한다.

page->tmp_page는 사용을 했으므로 NULL로 바꾸고, copy_page_from_iter() 함수를 통해 실제 데이터를 복사해온다.

만약 남은 데이터가 없다면 for문을 탈출한다.
남은 데이터가 있고, pipe가 꽉 차지 않았다면 continue한다.

그 후 나머지 코드는 pipe가 꽉 찬 경우에 해당하므로, 자기차례가 올 때까지 기다리는 부분에 해당한다.

pipe_read()

pipe_write와 코드가 굉장히 유사하다.
설명은 생략한다. (사실 그냥 귀찮아서..)

pipe_release()

readers와 writers 중 해당하는 값을 -- 한다.

만약 readers / writers 둘 중 하나만 0인 경우 모든 wait 중이었던 프로세스들을 깨우고, SIGIO를 발생시킨다(? 여기는 잘 모르겠음. 어쨌든 깨우는 건 맞는듯)

마지막에 put_pipe_info()를 호출한다.

put_pipe_info()

pipe->files 값을 -- 한 후, 만약 더 이상 참조하는 files가 없다면 free_pipe_info()를 호출하게 된다.

free_pipe_info()

for문을 돌면서 pipe_buf_release()를 호출한다.

마지막에 pipe, pipe->bufs를 모두 할당해제한다.

pipe_buf_release()

struct pipe_buf_operations에 저장되어 있던 release() 함수를 호출한다.

anon_pipe_buf_release()

일반적인 경우 이 함수가 호출된다.
buf의 page를 할당 해제하는 함수이다.
만약 pipe->tmp_pageNULL이라면 pipe->tmp_page에 저장한다.

앞서 pipe_write()에서 pipe->tmp_page를 사용했었는데, 여기서 저장되었던 것이다.

두 번째 인자가 buf?

매개변수를 보면, struct pipe_inode_info, struct pipe_buffer를 가지는 것을 확인할 수 있는데, 특히 두 번째 인자가 buf인 것에 주목할 필요가 있다.
만약 pipe_buffer에 arbitrary write가 가능하다면, rsi값이 buf인 것을 이용해 rsi를 rsp로 변환시키는 가젯을 이용하여 rop로 변환시킬 수 있게 된다.

자세한 내용은 다음 링크 참조.
https://velog.io/@dandb3/Linux-Kernel-pipebuffer%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EA%B8%B0%EB%B2%95%EB%93%A4

pipe_fcntl()

pipe의 buffer 크기를 조정하는 pipe_fcntl() 함수에 대해서도 알아본다.
우선 pipe 사이즈를 조정하려면 fcntl(pipefd, F_SETPIPE_SZ, size); 의 꼴로 함수를 호출해야 한다.
그러면 fcntl() 내부적으로 pipe_fcntl() 함수를 호출하게 된다.

F_SETPIPE_SZ의 경우 pipe_set_size() 함수를 호출한다.

pipe_set_size()

arg 값은 buffer의 size 값으로 들어온 인자이다.
이 때 1352 line을 보면 round_pipe_size() 함수가 호출되는데, 이 함수는 내부적으로 roundup_pow_of_two() 매크로를 호출하여 2의 제곱수로 round한 값을 리턴하게 된다.
앞서 ring_buffer 값을 2의 제곱수라고 말했는데, 바로 여기서 결정되는 것이다.

pipe_resize_ring() 함수를 통해 기존에 존재하던 pipe_buffer는 할당해제, 새로운 pipe_buffer를 할당받게 된다.

Reference

profile
공부 내용 저장소

0개의 댓글

관련 채용 정보