Thread
개요
- 쓰레드란?
- 프로세스 내의 실행 유닛
- 프로세스의 태두리 내에서 실행되며 같은 프로세스 내의 쓰래드간 코드와 전역 변수를 공유한다.
- 쓰레드는 상대적으로 프로세스보다 서로 데이터를 공유하는 것이 쉽다.
- 일반적으로 프로세스보다 더 효율적이다.
- 쓰레드간 메모리 공유
- user stack 은 공유하지 않음
- 함수 호출시 함수의 데이터가 쌓이는 메모리(지역변수 등)
- 힙 메모리를 공유
- 동적할당 메모리
- static data 영역 공유
- 전역변수, static 변수
- 코드영역 공유
- 프로그램의 코드 (함수, 제어문, 상수)
- user stack 은 공유하지 않음
관련 함수들
-
Thread 생성 함수:
pthread_create()
int pthread_create( pthread_t *thread, // 생성된 쓰레드 id 저장할 포인터 const pthread_attr_t *attr, // attribution 뒤에 설명 void *(*start)(void *), //쓰레드가 main으로 사용할 함수 void *arg // main함수에 전달할 데이터의 포인터 )
- return: 생성 성공시 0 실패시 error code
- *thread: 해당 프로세스 내에서 유효하게 사용할 수 있는 쓰레드 주소를 전달받을 수 있다.
- *attr: 생성할 쓰레드의 속성값을 명시할 수 있도록 하는 변수이다.
- pthread_attr_t 이라는 타입(구조체)는 다음의 방법으로 생성, 조작 가능
- 생성:
int pthread_attr_init(pthread_attr_t *tattr)
- 구조체 구성요소
- Detach state
초깃값: PTHREAD_CREATE_JOINABLE = joinable thread를 생성
pthread_attr_setdetachstate()
= detatch로 변경 가능 - Scope
초깃값: PTHREAD_SCOPE_SYSTEM = cpu자원 분배시 시스템 내 모든 쓰레드와 경쟁
pthread_attr_setscope()
= 프로세스 내 쓰레드와 경쟁하게 할 수 있음 - Inherit scheduler
초깃값: PTHREAD_INHERIT_SCHED: 자신을 생성한 쓰레드의 스케줄링 값을 상속 받음
pthread_attr_setinheritsched()
- Scheduling policy
초깃값: SCHED_OTHER: 새로운 쓰레드는 표준 스케줄링 규칙을 적용받음
pthread_attr_setschedpolicy()
- Scheduling priority
- Guard size
- Stack size
- Detach state
초깃값: PTHREAD_CREATE_JOINABLE = joinable thread를 생성
- 생성:
- pthread_attr_t 이라는 타입(구조체)는 다음의 방법으로 생성, 조작 가능
-
자신의 tid를 알려주는 함수
pthread_self()
pthread_t pthread_self()
- 프로세스 내에서 유효한 자신의 tid를 반환한다.
-
자신의 Thread를 종료하는 함수:
pthread_exit()
void pthread_exit(void *retval)
- 함수를 호출한 쓰레드를 종료시킨다.
- retval을 pthread_join()을 호출한 함수에 전달한다.
- pthread_join은 쓰레드의 잔해를 수습하고, retval을 전달받는 함수이다.
-
다른 Thread 종료시키는 함수:
pthread_cancle()
int pthread_cancel(pthread_t thread)
- thread에 쓰레드의 id (프로세스 내에서 유효한)을 넘겨주면 해당 쓰레드를 종료시킨다.
-
다른 Thread 의 종료를 기다리고 reap하는 함수:
pthread_join()
int pthread_join(pthread_t thread, void **value_ptr)
- 쓰레드의 종료까지 기다리다가(그동안 block) 종료되면 reaping
- return: 성공시 0, 실패시 error code
- thread: reap할 쓰레드 id를 전달
- value_ptr: 종료 상태를 전달받을 포인터
-
Thread에 signale을 보내는 함수:
pthread_kill()
int pthread_kill(pthread_t thread, sig_t signal)
- thread에 signal을 전송하는 함수
Thread 종류 (joinable)
- Joinable thread (default)
- 다른 쓰레드가 reap할 수 있고, 다른 쓰레드에 의해 reap 될 수 있다.
- 다른 쓰레드가 reap하기 전까지는 메모리가 해제되지 않는다.
- detached thread
- 다른 쓰레드가 reap이나 종료할 수 없다.
- 쓰레드 종료시 자동적으로 메모리가 해제된다.
int pthread_detach(pthread_t thread)
로 detach할 수 있다.
Thread Synchronization
- Mutex
개요
- 쓰레드간에 전역변수를 공유하기 때문에 전역변수에 동시에 접근할 경우 충돌이 발생할 수 있다.
- Critical section
- 공유되는 메모리에 접근하는 코드를 명칭
- 같은 메모리를 두고 경합하는 critical section은 반드시 여러 쓰레드에서 동시에 실행되지 않게 해야 한다.
- Mutual exclusion
- Critical section에 대한 독점권을 부여하는 것
- 어떤 쓰레드가 critical section에 진입했을 때, 동일한 자원에 대한 critical section에 다른 쓰레드의 접근을 막아주는 것
- Mutex
- 쓰레드 동기화를 구현하는 것을 뜻한다.
Mutex 변수
Mutex 생성과 초기화
-
static 생성
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 생성할 경우 PTHREAD_MUTEX_INITIALIZER값으로 초기화된다.
- 반드시 위의 값으로 초기화 해주어야 한다.
- 생성할 경우 PTHREAD_MUTEX_INITIALIZER값으로 초기화된다.
-
dynamic 초기화
int pthread_mutex_init( pthread_mutex_t *mutex // mu pthread_mutexattr_t *attr )
- 코드 실행중 어디서든 원하는 값으로 mutex를 초기화 할 수 있게 해주는 함수
- return: 성공시 0, 실패시 error code
Mutex destory
int pthread_mutex_destroy(pthread_mutex_t *mutex) //제거할 뮤텍스의 포인터
- mutex의 주소에 담긴 mutex 변수를 삭제한다.
- return: 성공시 0 실패시 error code
Mutex lock
int pthread_mutex_lock(pthread_mutex_t *mutex)
- mutex 변수의 상태를 확인하고 unlock 상태라면 lock 으로 바꾸어주고, 이미 lock 상태라면 unlock 상태가 될 때 까지 쓰레드가 sleep을 한다.
- return: 성공시 0 실패시 error code
int pthread_mutex_trylock(pthread_mutex_t *mutex)
- mutex_lock과 같으나 이미 mutex 가 lock 상태라면 sleep에 들어가지 않고 다음 으로 계속 진행한다.
Mutex unlock
int pthread_mutex_unlock(pthread_mutex_t *mutex)
- mutex 변수를 lock 상태에서 unlock 상태로 변경해주는 함수이다.
- return: 성공시 0 실패시 error code
- 여러 쓰레드가 한 mutex를 두고 sleep 중일때 우르르 깨우고 경합을 시키게 되어 이때 누가 차지하는가를 두고 문제가 발생하기도 한다.
File I/O
- open, read, write, close …
File Table
- 커널영역에 존재하는 open file들에 대한 list이다.
- 프로세스마다 하나씩 보유한다.
- file discriptors 라고 하는 integer으로 접근이 가능하다.
- File Table의 각 요소들은 파일에 대한 다음의 정보들을 가지고 있다.
- inode
- File offset
- Access modes
File discriptor
- C 언어에서는 integer type을 하고 있다.
- 0부터 시작해 1씩 증가하며 파일들의 위치를 가르킨다.
- 생성과 동시에 미리 할당된 인덱스들
- 0: stdin
- 1: stdout
- 2: stderr
- 생성과 동시에 미리 할당된 인덱스들
- 일반적인 파일 이외에도 IO 디바이스, pipes, directories, socket들도 관리할 수 있다.
IO함수
파일 열기: open()
int open(const char *name, int flags)
int open(const char *name, int flags, mode_t mode) //O_CREAT 전용
- return: 성공시 fd 반환, 오픈 실패시 -1 반환하고 errno에 적절한 값 세팅
- name: 열고자 하는 파일 이름 (절대경로, 상대경로 모두 가능)
-
flags: 필수적인 요소 1개와 optional 한 요소 들을 으로 조합하여 만들 수 있다. - 필수요소
- O_RDONLY: 읽기 전용
- O_WRONLY: 쓰기 전용
- O_RDWR: 읽기 쓰기 모두 가능
- 옵션
- O_APPEND: 파일을 열 때 off_set을 파일의 마지막으로 할당 (초깃값: 0)
- O_CREAT: 기존 파일이 있으면 그것을 열고, 없으면 새로 생성
- 파일접근권한도 명시해줘야 해서 mode에 그것을 명시
- 파일접근권한도 명시해줘야 해서 mode에 그것을 명시
- O_EXCL: 생성하되 동일한 파일이 있으면 -1반환 (exclusive)
- O_SYNC: write 명령으로 준 데이터가 실제 파일에 쓰였는지 매번 확인
- O_TRUNC: 기존 파일이 있다면 싹 지우고 새로 생성
- 필수요소
파일 열기(+생성): creat()
int creat(const char *name, mode_t mode)
// open(file, O_WRONLY|O_CREAT|O_TRUNC, mode) 와 동일
- return: 성공시 fd, 실패시 -1반환과 errno 세팅
파일 읽기: read()
ssize_t read(
int fd, // 읽을 파일의 fd
void *buf, // 읽은 내용을 담아올 버퍼
size_t len // 최대 얼마나 읽어올지에 대한 내용(상한)
)
- offset이 가리키는 곳에서부터 최대 len 만큼의 바이트를 읽어와(eof만나면 거기까지) buf에 담고, fd가 가리키는 파일 테이블에 가서 읽은 길이만큼 offset을 증가시킨다. buf의 마지막에 따로 ‘\0’를 추가해주진 않는다.
- return: 읽으려는 시도가 성공했을 경우 읽은 byte 만큼을 반환한다. offset이 EOF를 가리키는 상태라면 0을 반환한다. 읽는 도중에 다른 시그널의 개입 등으로 읽기가 중단되었을 때 읽기 성공한 바이트만큼을 반환한다. 그리고 errno를 EINTR으로 세팅한다.
- 만약에 읽으려는 시도는 성공했으나 누군가의 방해로 0바이트를 읽었다면 0을 반환할 것이다. EOF에 도달한 경우와의 차이점은
errno == EINTR
이라는 점이다.
읽기 실패시 -1을 반환하는데 시그널의 interrupt 인 경우일 수 있으니 -1을 받았다고 시스템 에러로 분류하는게 아니라 errno == EINTR인지 확인해 주는 것이 좋다. EINTR일 경우 read를 다시 시도한다. 다른 errno일 경우 손쓰기 어려우니 종료시키자.
- 만약에 읽으려는 시도는 성공했으나 누군가의 방해로 0바이트를 읽었다면 0을 반환할 것이다. EOF에 도달한 경우와의 차이점은
-
예제 코드
파일 쓰기: write()
ssize_t write(
int fd, // 쓰기 대상 fd
const void *buf, // 쓸 내용을 담은 버퍼
size_t count // buf에서 최대 얼마나 쓸 것인지
)
- 최대 count바이트 만큼을 buf에서 offset이 가리키는 위치에 작성한다. 매 바이트 작성하며 offset을 다음 바이트로 옮겨준다.
- buf에 NULL문자가 있다면 NULL문자를 작성하는 것이 아니라 그 문자 전까지만 작성된다.
- return: 성공시 작성한 바이트 크기만큼 반환하고 실패시 -1을 반환한다. 실패시 -1을 반환하고 errno를 세팅한다. read와 다르게 부분적 성공이 없고, interrupt도 일어나지 않아 -1을 반환받으면 불가능을 인정하고 에러처리하면 된다.
- 파일에 실재로 쓰는 행위는 delay되는데 쓰는데 비용이 상당하기 때문이다. 이때문에 일정 단위로 모아서 작성하기 위해 메모리 영역의 버퍼에 그 내용들을 임시 보관한다. 즉 write()가 -1을 반환하지 않았다고 해도 실제로 쓰여졌다는 것을 보장하지는 않는다.
page cache에 저장된 delaied들을 실행하라는 독촉: fsync()
int fsync(int fd)
- 현재 page cache에 계류중인 I/O 명령들을 실행하라는 명령이다.
- 그러나 이 함수 또한 명확히 실행을 보장하지 않고, device driver에 요청을 보낸는 수준까지만 해준다.
- 이게 할 수 있는 최선이다.
- 여러번 실행하면 성능하락을 각오해야 한다.
- return: 성공시 0 실패시 -1
파일 닫기: close()
int close (int fd)
- 열었던 파일을 닫으라는 명령이다. 파일을 닫은 후 해당 파일을 가리키던 fd는 다시 할당가능상태가 된다. 파일을 닫았다고 page cache의 명령들이 실행되는 것이 아니니 착각하지 말자
- return: 성공시 0 실패시 -1
File Offset
개요
- read()와 write 함수가 동작할 파일내 위치를 저장한다.
- 파일의 시작값에서부터 바이트 단위로 위치를 구분한다.
- 파일이 open 될 시 (append 없을 때)0을 가리킨다.
관련 함수
-
Offset 위치 재설정:
lseek()
off_t lseek( int fd, off_t pos, // 이동할 거리 (음수도 가능!) int origin // 이동의 기준 )
- fd의 file Offset을 지정한 위치로 이동시켜준다.
- return: 성공시 새로 설정된 offset값을 반환한다. 실패시 -1을 반환한다.
- origin: 이동의 기준을 잡는 변수이다.
- 종류
- SEEK_CUR: 현위치를 기준으로 Offset을 이동시킨다.
- 당연하겠지만 pos에 0가 온다면 이동시키지 않고 현위치를 반환한다.
- SEEK_END: 파일의 끝을 기준으로 pos 만큼 뒤로 Offset을 위치시킨다.
- SEEK_SET: 시작점을 기준으로 pos를 위치시킨다.
- SEEK_CUR: 현위치를 기준으로 Offset을 이동시킨다.
- 종류
-
Offset 과 독립으로 파일을 읽는 함수:
pread()
ssize_t pread ( int fd, void *buf, // 읽은 내용 저장할 버퍼 size_t count, // 최대 읽을 바이트 수 off_t pos // 읽을 절대 위치 )
- pos 위치에서부터 최대 count 만큼의 바이트를 읽어 buf에 저장, offset은 업데이트 되지 않는다.
-
Offset 과 독립으로 파일에 쓰는 함수:
pwrite()
ssize_t pwrite ( int fd, const void *buf, size_t count, 0ff_t pos )
- pos 위치에서부터 buf의 내용을 최대 count만큼 써주는 함수
-
lseek()
사용시 주의점- File Table은 프로세스마다 하나씩 보유하지만 File Table 의 fd는 운영체제가 갖는 글로벌 테이블에 있는 File Entry 를 가르키고(참조) 있다.
즉, open을 한 뒤 fork를 한뒤 한쪽에서 Offset를 바꿀 경우 다른 프로세스에도 그 영향이 미친다는 것이다. File Offset은 운영체제의 Global File Table에 있다고 할 수 있다.
- 이때 글로벌 테이블은 reference count를 보유하는데 fork 전에 count가 1이었다가 fork와 동시에 2로 증가하게 되고, child나 parent process에서 파일을 닫을 경우 count가 1 줄어들고, 0이 되지 않는 이상 테이블에서 소거되지 않아 다른 프로세스에서 파일에 접근하는데 제약이 없다.
- File Table은 프로세스마다 하나씩 보유하지만 File Table 의 fd는 운영체제가 갖는 글로벌 테이블에 있는 File Entry 를 가르키고(참조) 있다.
즉, open을 한 뒤 fork를 한뒤 한쪽에서 Offset를 바꿀 경우 다른 프로세스에도 그 영향이 미친다는 것이다. File Offset은 운영체제의 Global File Table에 있다고 할 수 있다.
기타 참고사항(size_t)
- size Limits
- cpu마다 자료형 단위가 다르게 정의되어 있어 하나의 코드를 안정성있게 실행하기 위해선 size_t라는 자료형을 사용하는 것이 안전할 수 있다.
- size_t 의 Limit : SIZE_MAX
- ssize_t 의 Limit: SSIZE_MAX
- cpu마다 자료형 단위가 다르게 정의되어 있어 하나의 코드를 안정성있게 실행하기 위해선 size_t라는 자료형을 사용하는 것이 안전할 수 있다.
기타 참고사항 하나를 여러번 open
- 하나의 파일을 open 할 때마다 Global File Table에 entry가 추가 된다.
- 즉, 이렇게 여러번 오픈하면 file offset가 중복사용되는 문제를 막을 수 있다.
- 당연히 프로세스 파일 리스트에 할당된 fd도 다르고 close또한 각각 해주어야 한다.
Multiplexed I/O
- select()
개요
- File descriptor은 여러 종류가 있다.
- 일반적인 File descriptor
- 특별한 File descriptor
- 모터나 센서, 키보드 등의 I/O단위가 바이트 단위인 장치
- 다수의 바이트 Block이 입출력 단위인 I/O
- Pipes
- Socket
- 어떠한 프로세스도 하나 이상의 file descriptor을 점유하는것은 불가능하다.
- read시스템 콜이 발생하면 그것의 실행 결과 (실패든, 성공이든)를 반환받기 전까지 프로세스 자체가 block(sleep)상태로 들어간다. 즉, 하나의 fd에서 결과를 받지 않고 다른 파일에 접근하는 것은 불가능하다.
- 여러개의 fd를 사용해야 할 때 그 목록들을 넘겨주고, 하나라도 사용 가능하면 그것을 사용하고, 모두 사용가능하지 않을 경우 잠에들지만, 하나라도 사용가능한 상태가 될 경우 깨어나는 기능이 필요
여러 File descriptor의 사용권한 질문: select()
int select(
int n, // 넘겨준 fd 중 가장 높은 값의 fd+1을 전달
fd_set *readfds, // read 가능한지 알고싶은 fd
fd_set *writefds, // write 가능한지 알고싶은 fd
fd_set *exceptfds, // exception발생여부를 알고싶은 fd (소켓에서 사용)
struct timeval *timeout // sleep에 들 시간의 상한 null 주면 무한정 기다림
)
// 리눅스는 timeout에서 기다린 시간만큼 차감하여 업데이트된 값을 담아줌
- 넘겨받은 set 들 중 사용가능한 것이 있으면 사용 가능한 fd의 개수를 반환하는 함수이며 사용가능한 것이 없을 경우 사용가능한 것이 나타나거나 timeout의 만기에 도달할 때 까지 기다린다.
- return: 정상적으로 실행될 경우 사용가능한 fd의 개수를 반환하고, timeout동안 하나도 나타나지 않을 경우0을 반환, 오류가 발생할 경우 -1을 반환하고 errno를 세팅하낟 errno == EINTER일 경우 다른 signal handler에 의해 방해받으 경우이다.
- fd_set은 비트단위로 저장되며 어떤 fd에 대한 값을 요청하는지에 대한 정보를 저장한다.
- fd_set의 메크로
FD_CLR(int fd, fd_set *set)
- set애서 fd를 제거한다.
FD_ISSET(int fd, fd_set *set)
- set에 fd가 사용가능한 상태로 표기되어 있는지 확인한다.
FD_SET(int fd, fd_set *set)
- set에 fd를 추가한다.
FD_ZERO(fd_set *set)
- set에 담긴 모든 내용을 소거한다.
시그널의 방해를 차단하는 select: pselect()
int pselect(
int n, // 넘겨준 fd 중 가장 높은 값의 fd+1을 전달
fd_set *readfds, // read 가능한지 알고싶은 fd
fd_set *writefds, // write 가능한지 알고싶은 fd
fd_set *exceptfds, // exception발생여부를 알고싶은 fd (소켓에서 사용)
const struct timespec *timeout, //sleep에 들 시간의 상한 null 주면 무한정 기다림
const sigset_t *sigmask // 여기 담긴 시그널의 개입을 함수 실행동안 무시한다.
)
Memory Mapped I/O
- mmap()
개요
- 프로그램의 메모리 영역에 파일의 내용을 그대로 옮겨와서 포인터에 접근하듯이 파일내용에 directly 접근하는 것을 가능하게 해줌
파일을 메모리에 매핑하는 함수: mmap()
void *mmap (
void *addr,
size_t len,
int prot,
int flags,
int fd,
off_t offset
)
- 메모리 상에 file의 offset부터 len 바이트 크기만큼을 매핑해주는 함수
- return: 성공시 실제로 매핑된 주소를 반환 (메모리의 시작 주소) 실패시 MAP_FAILED
- 매개변수들
- addr: 파일의 내용을 위치시킬 메모리 주소를 명시해준다.
- 일반적으로 0을 넘겨 줌으로 커널이 적절한 위치에 알아서 매핑하라는 지시를 준다.
- prot: 매핑된 메모리의 속성값을 명시해준다. chmot와 비슷…
- 반드시 fd를 open 시 넘겨주었던 속성값의 범위 내의 값으로 설정해주어야 한다. 읽기면 읽기, 쓰기면 쓰기, 읽기 쓰기면 둘다 또는 둘중 하나
-
PROT_READ, PROT_WRITE, PROT_EXEC 를 연산자 조합하여 넘겨준다.
- flags: 다양한 행동을 명시할 수 있음
- MAP_FIXED: 반드시 addr로 넘겨준 위치에 매핑하고 안되면 그냥 오류를 반환하라
- MAP_PRIVATE: 내가 매핑한 파일을 다른 프로세스가 사용하지 못하게 하라 (파일 업데이트의 즉시성이 낮아진다.)
- MAP_SHARED: 내가 매핑한 파일은 다른프로세스와 공유하는 파일이니 파일의 변경내용의 적용 우선순위를 높여서 다른 프로세스들이 업데이트된 내용을 읽게 하라
- offset: 파일에서 매핑될 내용의 시작위치, 이는 반드시 page-size의 배수로 할당되어야 한다.
- addr: 파일의 내용을 위치시킬 메모리 주소를 명시해준다.
매핑된 메모리의 해제: munmap()
int munmap (
void *addr, // 해제를 시작할 주소
size_t len // 얼마만큼 해제할 것인지
)
- addr에서부터 len만큼을 메모리에서 해제하라는 명령
- return: 성공시 0, 실패시 -1
참고
-
mmap은 글로벌 테이블의 ref count를 증가시키고 munmap은 ref count를 감소시킨다.
-
즉, mmap 이후 close가 있더라도 ref가 0이 되지 않아 계속 파일에 접근할 수 있다.
-
fstat(int fd, struct_stat *sb)
: 파일 디스크립터에 대한 정보를 sb 에 담아준다. -
S_ISREG(sb.st_mode)
: sb에 담긴 파일이 regular 파일(스토리지에 들어가있는 일반적 파일)인지 검사한다.
mmap의 장점
- low overhead: 유저영역에서 커널영역으로 커널에서 유저로 전환되는데 소모되는 오버헤드 감소
- easy to share: 여러 프로세스들이 파일 읽고 쓰기를 메모리접근하듯, 빠른 업데이트도 되고 좋음
- easy to seek: 포인터를 조작하여 메모리에서 이동하기 때문에 파일내 위치이동이 자유로움
mmap의 단점
- wate of space: 몇 바이트 짜리 파일을 매핑하든 4kb(page size)가 소모됨
- Limited mapping size: 메모리 공간을 차지하기 때문에 큰용량을 매핑할 수 없다.
- High overhead: 외부적으로 적은 오버헤드가 소모되는 것 처럼 보일 수 있으나 내부적으로 그걸 달성하기 위해 매우 분주한다. (그럼에도 장점의 영향이 더크다.)
I/O Redirection
-
, », 2>, >&, dup
I/O operations
-
- 좌항의 실행 결과로 stdout에 출력될 내용을 stdout이 아닌 우항의 파일에 덮어씌워준다.
-
- 좌항의 실행 결과로 stdout에 출력될 내용을 우항의 파일에 append로 출력
- 2>
- 좌항의 실행 결과로 stderr에 출력될 내용을 우항의 파일에 덮어씌워준다.
-
&
- 좌항의 실행 결과로 stderr, stdout에 출력될 내용을 우항의 파일에 덮어씌워준다.
File descriptor을 가능한 가장 낮은 fd에 하나 더 할당: dup()
int dup (int oldfd) // 재할당할 fd
- 이미 할당이 완료된 fd에 담긴 파일을 지금 할당 가능한 새로운 fd에 배정. 기존 fd도 유지 즉 newfd = dup(1);을 실행하면 newfd엔 3이 할당되어 1과 3에 stdout이 배정된다.
- return: 성공시 재할당된 fd, 실패시 -1
-
예제
close(2); newfd = dup(1);
stderr에 출력될 내용들이 stdout으로 redirection된다.
복사하여 할당할 fd를 명시할 수 있는 dup: dup2()
int dup2(
int oldfd, // 복사할 IO의 fd
int newfd // 복사할 위치
)
- oldfd를 newfd에 복사해준다. 만약 newfd가 이미 점유중이라면 기존의 file을 close하는 실행도 해준다.
dup2(1, 2);
==close(2); dup(1);
- return: 성공시 newfd, 실패시 -1
global file table과 dup의 관계
- 단순히 dup()나 dup2()로 복사된 fd들은 global file table에서 같은 테이블을 가리킨다.
- 즉, 같은 offset을 공유하고 ref count만 증가한다는 의미이다.
- global file table을 새로 생성하는 명령은 아직까지 배운 범위 내에선 open밖에 없다.
stdio: Standard I/O Library
개요
- Performance issues
-
Alignment
파일은 기본적으로 block 이라는 단위로 관리된다. write 하려는 내용의 시작 주소, 크기가 block의 배수일 때 가장 좋은 속도를 낸다.
-
Number of system calls
read write는 프로세스 영역에서 시스템영역으로 일종의 컨텍스트가 이동하는 행위이다. 한번에 묶어서 가능한 적은 횟수로 처리하는게 좋은 성능을 보여준다.
-
- stdio: User-buffered I/O
- 시스템 콜 횟수를 줄이기 위해 유저의 영역에서 IO 데이터를 머금고 있다가 한번에 명령을 내보내는 것. 라이브러리 수준(= 유저수준)의 버퍼링
- stdio는 플렛폼 독립적인 유저수준 버퍼링 기능을 제공한다.
-
개략도
- 내부적으로 File discriptor을 FILE 이라는 타입의 포인터로 매핑하여 사용한다.
I/O 함수들
-
파일을 열어주는 함수:
fopen()
FILE *fopen ( const char *path, //열고자 하는 파일의 경로 const char *mode // 어떤 모드로 파일을 열 것인지 )
- mode 값들
- r: 읽기모드로 파일을 연다.
- r+: 읽기와 쓰기모드로 파일을 연다. 이때 파일이 있다면 내용을 유지하지만, 없다면 만들지 못하고 실패한다.
- w: 쓰기모드로 파일을 연다.
- w+: 읽기와 쓰기모드로 파일을 연다. 이때 이미 파일이 있다면 삭제하고 다시만들고, 없다면 새로 만든다
- a: append 모드로 파일을 연다. offset은 파일의 끝을 가리킨다. 파일이 없다면 새로 만든다.
- a+: 읽기와 쓰기모드로 파일을 연다. 파일이 없다면 새로 만든다. 당연히 이미 있다면 내용 유지된다.
- return: 성공시 FILE pointer, 실패시 NULL
- mode 값들
-
fd를 FILE *로 바꿔주는 함수:
fdopen()
FILE *fdopen (int fd, const char *mode)
- 이미 열린 fd를 file descriptor으로 바꿔준다.
- return: 성공시 File *, 실패시 NULL
- mode: open으로 열며 명시한 mode의 범위에 포함되어야 한다.
-
FILE 에서 1글자를 읽어주는 함수:
fgetc()
int fgetc (FILE *stream)
- stream에서 1글자를 읽어서 반환한다.
- return: int형식으로 unsigned char을 변환해서 전달해주는데 그 이유는 오류발생시 오류코드를 반환해야 하기 때문이다.
-
FILE 에서 문자열을 읽어주는 함수:
fgets()
char *fgets( char *str, // 저장할 버퍼 int size, // 읽을 크기+1 *** size -1개를 읽고 마지막에 \0을 추가한다. FILE *stream )
- stream에서 최대 size -1개의 문자를 읽어 str에 저장하고 str의 마지막에 ‘\0’을 추가하여 총 size개의 문자가 str에 들어간다.
- *** 이때! \n을 읽더라도 큰 의미를 두지 않고 하나의 문자로 인식한다. 특별한 동작 없다.
- return: 성공시 str을 반환, 실패시 NULL반환
-
파일을 특정한 사이즈 단위로 읽어주는 함수:
fread()
size_t fread ( void *buf, //읽은 내용을 저장할 버퍼 size_t size, // 읽기를 수행할 단위가 되는 구조의 크기 size_t nr, // 몇개의 구조를 읽을지. FILE *stream )
- nr개의 size 크기 구조채를 익어서 buf에 담아준다.
- return: 몇개의 구조를 읽었는지 반환한다.
- 아까 언급되었던 block단위 읽기나 구조체단위 읽기할 때 쓰면 좋을 것 같다.
-
FILE 에 글자 1개를 쓰는 함수:
fputc()
int fputc(int c, FILE *stream);
- 파일에 c에 담긴 글자 1바이트를 작성한다.
- return: 성공시 c, 실패시 EOF
-
FILE 에 문자열을 입력하는 함수:
fputs()
int fputs (const char *str, FILE *stream)
- str의 마지막은 반드시 ‘\0’가 위치해야 한다!
- stream에 str의 내용을 입력하는데 ‘\0’을 만날때까지 계속 입력한다.
- return: 성공시 양수를 반환, 실패시 EOF를 반환
-
FILE 에 특정한 사이즈 단위로 쓰는 함수:
fwrite()
size_t fwrite ( void *buf, size_t size, // 작성할 구조의 크기 size_t nr, // 몇개의 구조를 작성할지 FILE *stream )
- 마찬가지로 size단위로 buf내용을 nr번 뽑아 stream에 작성한다.
- return: 성공시 nr, 실패시 nr이 아닌 모든값
- fread와 다르게 write는 모 아니면 도 이기 때문에 nr을 다 쓰지 못한 상황은 오류상황이다.
-
유저레벨에서 버퍼링 중인 것을 실행시키라는 명령:
fflush()
int fflush (FILE *stream)
- 유저버퍼영역에 계류중인 내용들을 커널영역으로 보내라는 명령
- return: 성공시 0, 실패시 EOF
- fsync는 운영체제에 직접이야기 하는것이고 fflush는 유저영역이다.
-
FILE 의 offset을 이동시키는 함수:
fseek()
int fseek ( FILE *stream, long offset, // 이동할 크기 int whence // 이동의 기준 )
- whence를 기준으로 offset만큼 떨어진 거리에 stream의 오프셋을 이동시켜준다.
- return: 성공시 0, 실패시 -1
- whence
- SEEK_SET: 파일의 시작, 즉 0을 기준으로 이동.
- SEEK_CUR: 현재 위치를 기준으로 이동.
- SEEK_END: 파일의 마지막위치를 기준으로 이동
-
FILE 의 offset 위치를 얻어주는 함수:
ftell()
long ftell (FILE *stream)
- stream의 오프셋의 현재 위치를 반환한다.
- return: 성공시 offset의 현재 위치를 반환, 실패시 -1을 반환.
fclose(FILE *stream)
: 파일 포인터를 반환하는 함수fcloseall(void)
: stdin, stdout, stderr을 포함한 모든 파일 포인터를 close
Time
시간의 종류
- Wall time
- 이벤트의 시간이나 유저와 소통에 사용하는 시간이다.
- absolute time 즉 프로세스나 cpu동작과 관계 없이 계 바깥의 시간
- 사용자가 변경하는 것이 가능하다.
- Monotonic time
- 시간의 간격이 일정하고 항상 증가하기만 하는 시간. 사용자가 임의조작하는것이 불가능하다.
- System’s uptime
- 특정한 계 내에서 시간을 측정하는데에 유용하다
- 시간의 간격이 일정하고 항상 증가하기만 하는 시간. 사용자가 임의조작하는것이 불가능하다.
- Process time
- 프로세스가 CPU 자원을 소모한 시간으로 CPU자원을 할당받지 않고 있을 경우엔 흐르지 않는다.
- 프로세스 성능을 파악하는 등의 용도로 사용한다.
시간을 측정하는 하드웨어 장치
- Real Time Clock (RTC)
- 현실의 시간, 즉 Wall time을 측정하는데 사용될 수 있으며
- 하드웨어가 꺼져 있는동안에도 시간은 흘러야 한다.
- 정밀도가 높지 않은 편이다.
- High Precision Event Timer (HPET)
- periodic interrups를 발생시킬 수 있는 상당히 정밀한 시계이다.
- 하드웨어가 종료될땐 동작을 하지 않는다.
- Time Stamp Counter (TSC)
- 시피유 사이클이 몇번 일어났는지 계측하는 시계이다.
- HPET보다 높은 정밀도를 갖지만, periodic interrupt를 제공하지는 않는다.
하드웨어 장치를 이용하는 소프트웨어: System Clock
- 리눅스에서는 system clock의 최소 단위 시간(tick)을 HZ라는 단위로 표기한다.
- default 값은 250HZ로 4ms에 해당한다.
- 1000HZ는 1ms이다.
- System Time
- Unix time인 1970.1.1을 기준으로 몇 초가 지났는지에 대한 시간 단위이다.
- Wall time에 해당한다.
- Unix 유래 time 계측
-
timeval structure
timeval
struct timeval{ time_t tv_sec; // 초단위 시간 suseconds_t tv_usec; // 마이크로 초단위 시간 };
-
현재 시간을 가져와 주는
gettimeofday()
int gettimeofday( struct timeval *tv, // 시간을 받을 장소 struct timezone *tz // 현재 사용하지 않음 Null주면 된다. )
- 현재 시간을 받아서 tv에 대입해주는 함수
- return: 실패시 -1
-
현재 시간을 설정하는
settimeofday()
int settimeofday( const struct timeval *tv, const struct timezone *tz )
-
- POSIX 유래 time 계측
- POSIX Clocks
- CLOCK_REALTIME
- 정밀도: 4ms
- Wall time을 얻어온다.
- CLOCK_MONOTONIC
- 정밀도: 4ms
- 부팅후 부터의 시간을 얻어온다.
- 이 시간은 유저가 재설정할 수 없다.
- CLOCK_PROCESS_CPUTIME_ID
- 정밀도: 1 나노초
- 해당 프로세스가 cpu를 사용한 시간을 받아온다.
- 프로세스 안 쓰레드들은 같은 값을 공유한다.
- CLOCK_THREAD_CPUTIME_ID
- 정밀도: 1 나노초
- 호출한 쓰레드가 CPU를 사용한 시간을 받아온다.
- 쓰레드간 각각의 값을 갖는다.
- CLOCK_REALTIME
-
timespec structure
timespec
struct timespec{ time_t tv_sec; // 초단위 시간 보관 long tv_nsec; // 나노초 단위 시간 보관 };
- 나노 초 단위 시간을 보관하여 더 높은 정밀도를 표현할 수 있다.
-
POSIX Clock의 정밀도를 알려주는
clock_getres()
int clock_getres( clockid_t clock_id, // 알고싶은 posix 시계 struct timespec *res // 정밀도를 저장할 공간 )
- 전달받은 시계의 정밀도를 res에 담아준다.
-
현재 시간을 얻어주는
clock_gettime(clock_id, timespec)
int clock_gettime( clockid_t clock_id, struct timespec *ts )
- clock_id가 측정하고 있는 현재 값을 ts에 저장해준다.
-
시계를 설정하는
clock_settime(clock_id, timespec)
int clock_settime( clockid_t clock_id, const struct timespec *ts )
- clock_id가 보관하고 있는 값을 ts에 보관된 값으로 변경해준다.
- 이 명령을 실행하려면 루트권한이 있어야 한다.
- 당연하게도 clock_monotonic은 바꿀 수 없다.
- POSIX Clocks
Sleep
- sleep: 잔다. CPU 자원을 배정받지 않는다
초단위로 잠을 재우는 sleep()
unsigned int sleep(unsigned int seconds)
- seconds만큼 재우고 시간이 흐른 뒤 깨운다.
- return: 알맞게 잤다면 0을 반환, 덜 잤다면 덜 잔 시간을 반환
나노초 단위로 잠을 재우는 nanosleep(timespec *req, timespec *remain)
int nanosleep(
const struct timespec *req, // 얼마나 재울지
struct timespec *rem // 일찍일어났다면 얼마나 일찍인지 저장해준다.
)
- nano초 단위로 잠을 자고 중간에 잠이 깬 경우 잔여시간을 rem에 저장해준다.
- CLOCK_REALTIME을 사용하여 계측한다.
- return: 잘 잤다면 0 중간에 깼다면 -1
특정 시계를 기준으로 잠을 재우는 clock_nanosleep(clock_id, flag, req, rem)
- clock_id에 명시된 시간을 기준으로 잠을 자고 일어나게 해준다.
- ** 중요!! clock_id에 CLOCK_PROCESS_CPUTIME_ID이나 CLOCK_THREAD_CPUTIME_ID이 올 수 없다. → 영원히 자게된다.
- select()로도 잠을 재울 수 있다. (unix clock 사용)
- 일정 초 뒤에 프로세스에 SIGALARM을 주는
alarm()
- 이미 설정된 alarm이 작동 중이라면 오래된 알람 해제하고 새 알람으로 교체
- 실행한다고 잠들지 않고 SIGALARM만 예약해주는 함수
Timers
- alarm()보다 많은 기능을 제공한다.
Interval Timer UNIX기반
-
itimer 구조체
itimerval
struct itimerval{ struct timeval it_interval; // 타이머의 간격 struct timeval it_value; // 다음 expire의 간격 0일경우 한번만 동작 };
-
itimer설정
setitimer(which, itimerval newval, itimerval oldval)
int setitimer( int which, // 타이머 모드 const struct itimerval *value, // 세팅값 struct itimerval *ovalue // 이미 세팅된것이 있다면 그건 여기에 보관 )
- mode
-
ITIMER_REAL
real Time을 기준으로 SIGALARM 시그널을 보낸다. (alarm과 유사)
-
ITIMER_VIRTUAL
프로세스가 유저 space에서 돌아간 시간을 기준으로. 만기시 SIGVTALRM을 보낸다.
-
ITIMER_PROF
유저레벨과 커널레벨에서 돌아간 시간을 기준으로 측정, SIGPROF를 보낸다.
-
- mode
-
itimer에 저장된 값을 가져오기
getitimer(which, itimerval)
- which 모드에서 동작중인 itimer의 정보를 가져오는 함수
Interval Timer POSIX기반
- Unix기반 타이머들은 간단하지만 overrun에 대응하지 못하고, 여러개 만드는데 제한이 있다.
- 이를 해결해주는 POSIX itimer가 있으니 걱정하지말자
-
구조체
itimerspec
struct itimerspec{ struct timespec it_interval; struct timespec it_value; };
-
Itimer 생성 timer_create(clockid, * sigevent, * timerid)
int timer_create( clockid_t clockid, //기반이 될 posix 시계 struct sigevent *evp, // expire시 어떻게 알려줄 것인가 timer_t *timerid // 생성된 itimer id를 전달받을 곳 )
- evp
- SIGEV_NONE: expire시에도 아무런 동작을 취하지 않는다.
- SIGEV_SIGNAL: expire시 evp.sigev_signo에 저장된 시그널을 발생
- SIGEV_THREAD: evp.sigev_notify_function을 메인으로 하는 쓰레드를 생성한다.
- SIGEV_THREAD_ID: evp.sigev_signo를 evp.sigev_notify_thread_id에 명시된 쓰레드에 보내라
- null을 줄 경우: SIGALRM을 발생
- evp
-
Itimer arm
int timer_settime( timer_t timerid, int flags, const struct itimerspec *value, struct itimerspec *ovalue )
- overrun횟수를 반환
int timer_getoverrun(timer_t timerid)
- delete timer
int timer_delete(timer_t timerid)
There are currently no comments on this article
Please be the first to add one below :)
내용에 대한 의견, 블로그 UI 개선, 잡담까지 모든 종류의 댓글 환영합니다!!