pipe를 만드는 syscall이다.
둘 다 공통적으로 do_pipe2()
를 호출한다.
두 syscall의 차이점은 flag
를 인자로 주는지에 대한 여부 차이이다.
__do_pipe_flags()
함수를 통해 파이프를 생성한다.
그 후, copy_to_user()
를 통해 생성된 새로운 fd
값을 user에게 되돌려주고, fd_install()
을 통해 새로 할당받은 fd
값과 새로 생성된 file
끼리 매칭시켜준다.
create_pipe_files()
를 통해 새로운 file
을 생성한다.
get_unused_fd_flags()
를 통해 2개의 새로운 fd
값을 할당받는다.
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
로 설정하게 된다.
위와 같은 전역 변수에 해당한다.
각 멤버에 대해서는 뒤에 자세히 살펴본다.
alloc_pipe_info()
를 통해 새로운 struct pipe_inode_info
를 할당받는다.
나머지는 멤버변수 초기화에 해당한다.
설명이 워낙 잘 써져 있어서 위 사진으로 대체한다.
pipe의 경우 ring-buffer로 관리된다.
내부 변수들을 보면, bufs
변수를 통해 실제 ring-buffer가 저장되고,
head
, tail
, ring_size
등의 변수들을 통해 ring-buffer를 관리하는 것을 확인할 수 있다.
pipe
의 핵심 구조체들을 할당하는 부분이다.
struct pipe_inode_info
구조체를 새로 할당한 후, struct pipe_buffer
구조체를 할당하게 된다.
여기서 눈여겨 볼 점은 할당 시 kzalloc()
을 통해 할당받고, GFP_KERNEL_ACCOUNT
플래그를 통해 kmalloc-cg-xx
의 slab cache를 (운영체제가 제공하는 경우) 사용하게 된다.
각 멤버변수에 대한 설명은 사진으로 대체한다.
기본적으로 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_info
의 ring_size
변수값이 pipe
의 사이즈를 의미하는데, 항상 2의 제곱수로 rounded 된다.
플래그 값 중 PIPE_BUF_FLAG_CAN_MERGE
는 기억해 두도록 하자. 나중에 쓸 예정.
이제 pipefifo_fops
에 대해 자세히 알아보자.
생성된 pipe
에 대한 여러 operations가 저장되어 있다.
몇 가지 operations에 대해 살펴보자.
함수가 길어서 부분별로 살펴볼 것이다.
만약 pipe
가 비어있지 않고, write할 데이터도 PAGE_SIZE
의 배수가 아니라면 마지막 buffer에 내용을 이어붙인다.
여기서 mask
값을 pipe->ring_size - 1
로 한 이유는 ring_size
자체가 2의 제곱수이기 때문에 (앞에서 설명함) 1만 빼면 마스크 값으로 쓸 수 있는 것이다.
그래서 이전 buffer의 주소를 구할 때 &pipe->bufs[(head - 1) & mask]
의 꼴로 쓸 수 있는 것임.
만약 이전 buffer의 flag
중 PIPE_BUF_FLAG_CAN_MERGE
가 set 되어 있었고, chars
와 offset
의 합이 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_write와 코드가 굉장히 유사하다.
설명은 생략한다. (사실 그냥 귀찮아서..)
readers와 writers 중 해당하는 값을 -- 한다.
만약 readers / writers 둘 중 하나만 0인 경우 모든 wait 중이었던 프로세스들을 깨우고, SIGIO
를 발생시킨다(? 여기는 잘 모르겠음. 어쨌든 깨우는 건 맞는듯)
마지막에 put_pipe_info()
를 호출한다.
pipe->files
값을 -- 한 후, 만약 더 이상 참조하는 files가 없다면 free_pipe_info()
를 호출하게 된다.
for문을 돌면서 pipe_buf_release()
를 호출한다.
마지막에 pipe
, pipe->bufs
를 모두 할당해제한다.
struct pipe_buf_operations
에 저장되어 있던 release()
함수를 호출한다.
일반적인 경우 이 함수가 호출된다.
buf의 page를 할당 해제하는 함수이다.
만약 pipe->tmp_page
가 NULL
이라면 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의 buffer 크기를 조정하는 pipe_fcntl()
함수에 대해서도 알아본다.
우선 pipe 사이즈를 조정하려면 fcntl(pipefd, F_SETPIPE_SZ, size);
의 꼴로 함수를 호출해야 한다.
그러면 fcntl()
내부적으로 pipe_fcntl()
함수를 호출하게 된다.
F_SETPIPE_SZ
의 경우 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
를 할당받게 된다.