[Ch03 버퍼 입출력]
블록 : 파일 시스템의 최소 저장 단위를 나타내는 추상 개념. 파일 시스템 연산은 블록 단위로 일어난다. (데이터에 필요한 블록이 4.5개라 하더라도 5개를 써야함.)
→ 블록의 일부분만 다루는 연산이 비효율적임.
3.1 사용자 버퍼 입출력
- 일반 파일에 대해 잦은 입출력을 처리해야만 하는 프로그램은 종종
사용자 버퍼 입출력
을 수행한다. - 이는 커널이 아니라
사용자 영역
에서 버퍼링
을 처리한다는 의미커널은 내부적으로 지연된 쓰기연산, 미리읽기, 연속된 입출력 요청 을 모아서 처리하는 방식으로 버퍼링을 구현하고 있음.
일반 파일에 대해 잦은 입출력을 처리해야 하는 프로그램은 종종 사용자 버퍼 입출력을 수행한다.
1
| dd bs=1 count=2097152 if=/dev/zero of=pirate
|
2MB 데이터를 1B씩 약 2백만 번에 걸쳐 읽어들임
1
| dd bs=1024 count=2048 if=/dev/zero of=pirate
|
2MB 데이터를 1KB씩 약 2천 번에 걸쳐 읽어들임
블록 크기 (Byte) | 실제 시간 (초) | 사용자 시간 (초) | 시스템 시간 (초) |
---|
1 | 18.707 | 1.118 | 17.549 |
1024 | 0.025 | 0.002 | 0.023 |
1130 | 0.035 | 0.002 | 0.027 |
1KB 단위로 읽어들이면 시스템콜의 횟수를 1024배 줄임으로써 성능을 비약적으로 개선할 수 있다.
다만 블록 크기를 1130 Byte로 키우면 시스템콜의 횟수는 줄지만 실제 물리 블록의 크기의 약수나 배수가 아니므로 성능 저하가 발생한다. 실제로 /dev/zero
의 경우 블록 크기는 4096 Byte다.
3.1.1 블록크기
- 실제로 블록 크기는 보통 512, 1024, 2048, 4096 혹은 8192로 정해진다.
- → 커널과 하드웨어는 블록 크기를 기준으로 대화하기 때문에 블록 크기의 정수배나 약수 단위로 연산을 수행하기만 해도 상당한 성능 개선이 따라옴
- 그렇다면 모든 데이터를
4KB
or 8KB
단위로 취급하는게 좋은가?- No. 실제로 데이터를 블록 단위로 취급하는 프로그램이 드물기에 현실성이 없음
- 프로그램은 블록 같은 추상 개념이 아니라 필드, 행, 단일 문자를 다룬다. 그래서
사용자 버퍼 입출력
이 필요함.
1
2
3
4
| 데이터가 쓰여지면 프로그램 **주소 공간 내 버퍼**에 저장이 됨.
버퍼가 특정 크기에 도달하면 전체 버퍼는 **한 번의 쓰기 연산을 통해 실제로 기록이 됨.**
읽기 또한 버퍼 크기에 맞춰 **블록에 정렬된 데이터를 읽는다.**
|
→ 데이터가 많더라도 모두 블록 크기에 맞춰 적은 횟수의 시스템 콜만 사용하게 됨. 성능 향상!
사용자 애플리케이션 코드 레벨에서 인위적으로 버퍼링을 구현해서 사용해야함.
But, 표준 입출력 라이브러라 (stdio)와 표준 C++ iostream이라는 견고하고 뛰어난 사용자 버퍼링 구현체를 가져다 사용하면 된다!
3.2 표준 입출력
- 표준 C 라이브러리는 표준 입출력 라이브러리 (
stdio
)를 제공함
3.2.1 파일 포인터
- 표준 입출력 루틴은 File Descriptor를 직접 다루지 않고, File pointer라는 독자적인 식별자를 사용한다.
- 표준 입출력 용어로 열린 파일은
Stream
이라고 부르기도 함.- Stream 은 읽기(입력 스트림), 쓰기 (출력 스트림), 또는 읽기/쓰기 (입출력 스트림) 모드로 열 수 있음
3.3 파일 열기
- 파일을 읽거나 쓰기 위해서
fopen()
을 사용한다. - (FILE은 stdio.h 에 정의된 FILE typedef)
1
2
3
4
5
6
7
8
| FILE * fopen (const char *path, const char *mode)
// EX
FILE *stream;
stream = fopen ("/etc/manifest", "r");
if (!stream)
ERROR
|
- 파일 path를 mode에 따라 원하는 용도로 새로운 스트림을 만든다.
- 성공 시 유효한 FILE 포인터를 반환.
- 실패 시 NULL 반환, errno 설정
3.3.1 모드
r
: 읽기 목적으로 파일을 엶.r+
: 읽기/쓰기 목적. 스트림은 파일 시작 지점.w
: 쓰기 목적으로 파일을 엶. 파일이 이미 존재하면 길이를 0으로 잘라버림. 파일이 존재하지 않으면 새로 만듬.w+
: 읽기/쓰기 목적. 파일이 이미 존재하면 길이를 0으로 자름. 파일이 존재하지 않으면 새로 만듦. 스트림은 파일 시작 지점.a
: 덧붙이기 상태에서 쓰기 목적으로 파일을 엶.a+
: 덧붙이기 상태에서 읽기/쓰기 목적으로 파일을 엶. 파일이 존재하지 않으면 새로 만듦. 스트림은 파일 끝 지점.
3.4 파일 디스크립터로 스트림 열기
fdopen()
함수는 이미 열린 파일 디스크립터를 통해 스트림을 만든다.
1
| FILE * fdopen (int fd, const char *mode);
|
- 사용가능한
mode
는 fopen()과 동일하며, 원래 fd를 열 때 사용했던 모드와 호환성을 유지해야 한다.- fopen()에서는 w모드로 스트림을 열었을 때 이미 존재한다면 파일을 0으로 잘라버렸음. 하지만 fdopen()은 그렇지 않은데 그 이유는 이미 파일이 fd에 대해서 열려있기 때문.
- 따라서 open() 함수에 의해 반환 받은 fd를 fdopen() 함수에서 받았을 경우 open() 함수에
O_TRUNC
플래그가 있어야만 파일을 자를 수 있음!
- fd가 스트림으로 변환되면 그 fd를 통해 직접 입출력을 수행이 가능하긴 하지만 그렇게 하면 안됨!
3.5 스트림 닫기
1
| int fclose (FILE *stream)
|
- 버퍼에 쌓여있지만 아직 스트림에 쓰지 않은 데이터를 먼저 처리함.
- fclose 하면 fd까지 닫히나?
- 성공하면 0 반환, 실패하면 EOF 반환하고 errno 적절한 값으로 설정
3.5.1 모든 스트림 닫기
fcloseall()
함수는 stdin, sdtout, stderr 를 포함해서 현재 프로세스와 관련된 모든 스트림을 닫는다.- 닫기 전에 버퍼에 남아 있는 데이터는 모두 스트림에 쓰여지며 언제나 0을 반환
3.6 스트림에서 읽기
- 스트림에서 데이터를 읽으려면 w나 a를 제외한 나머지 모드(
읽기 가능 모드
)로 스트림을 열어야 함
3.6.1 한 번에 한 문자씩 읽기
1
| int fgetc(FILE *stream)
|
- stream 에서 다음 문자를 읽고 unsigned char 타입을 int 타입으로 변환해서 반환한다.
- 타입 변환 이유 : 파일 끝이나 에러를 알려줄 수 있도록 하기 위함. 이런 에러일 때는
EOF
반환 - 반드시 반환 값이
int
타입이어야 한다. char
타입으로 저장하게 되면 에러 확인이 불가능함!
1
2
3
4
5
6
7
| int c;
c = fgetc (stream);
if (c == EOF)
// error
else
printf()
|
읽은 문자 되돌리기
- 스트림을 찔러보고 원하는 문자가 아닌 경우 되돌려버린다.
- 즉, 스트림에 문자를 다시 집어넣는 것임.
1
| int ungetc (int c, FILE *stream)
|
1
2
3
4
5
6
7
8
9
| // 여러번 호출 시 역순으로 출력. LIFO (Last In First Out)
// 파일에 직접 쓰여지는 것이 아니라 버퍼에 쓰여지게 됨
// 리눅스에서는 메모리가 허용하는 범위 내에서 무제한 되돌리기 허용
ungetc('a', stream);
ungetc('b', stream);
ch = getc(fp); // ch 에는 b 가 들어간다.
ch = getc(fp); // ch 에는 a 가 들어간다.
|
1
2
3
4
5
6
7
8
9
10
| // 중간에 파일 위치 표시자의 값이 0이 된다면 그 이후에 호출된 unget함수들은 모두 무시됨.
fp = fopen("test.txt", "r");
getc(fp); // 이 함수 호출 이후 위치 표시자의 값은 1
ungetc('a', fp); // 이 함수 호출 이후 값은 0
ungetc('b', fp); // 따라서 버퍼에 b 가 들어갈 수 없다.
ch = getc(fp); // ch 에는 a 가 들어간다.
printf("%c", ch);
ch = getc(fp); // ch 에는 test.txt 의 두 번째 문자가 들어간다.
|
ungetc()
를 호출하고 중간에 탐색함수를 호출했고, 읽기 요청은 아직 하지 않았을 경우 되돌린 문자를 다 버린다.- 스레드는 버퍼를 공유하므로 단일 프로세스에서 여러 스레드가 동작하는 경우에도 동일한 현상 발생
3.6.2 한 줄씩 읽기
fgets()
함수는 stream에서 문자열을 읽는다.
1
| char *fgets (char *str, int size, FILE *stream)
|
- stream에서 size보다 하나 적은 내용을 읽어서 결과를 str에 저장한다.
- 마지막 바이트를 읽고 난 다음, 버퍼 마지막에 null 문자 (\0)을 저장한다.
- EOF나 개행문자를 만나면 읽기 중단. 개행문자를 읽으면 str에 \n을 저장
- 무조건 \0은 마지막에 넣음.
- 문자열은 마지막에 NUll로 끝남
- 성공하면 str을 반환, 에러일 경우
NULL
반환
원하는 만큼 문자열 읽기
- 행 단위로 읽는 방법은 유용하지만 다른 구분자를 사용하고 싶을 때도 있음
- fgetc 로 fgets와 동일한 로직을 구현할 수 있다
1
2
3
4
5
6
7
8
9
10
| char *s;
int c;
s = str
// n-1 바이트를 읽어서 str에 저장
while (--n > 0 && (c = fgets (stream)) != EOF)
*s++ = c;
// \0 을 추가
*s = '\0';
|
1
2
3
4
5
6
7
8
9
|
// d를 \n으로 하면 fgets와 동일
while (--n > 0 && (c = fgec (stream) != EOF && (*s++ = c) != d)
;
if (c==d)
*--s = '\0';
else
*s = '\0';
|
3.6.3 바이너리 데이터 읽기
- 개별 문자나 행을 읽는 기능만으로 부족할때 (C 구조체 같은 복잡한 바이너리 데이터를 읽고 써야하는 경우)
fread()
함수 사용
1
| size_t fread( void *buf, size_t size, size_t nr, FILE *stream)
|
- stream에서 각각 크기가 size 바이트인 엘리먼트를 nr개 읽어서 buf가 가리키는 버퍼에 저장한다.
- 읽어들인 엘리먼트 개수가 반환됨.
- nr보다 적은 값을 반환하여 실패나 EOF를 반환
ferror()
or feof()
를 사용하지 않고서는 실패 or EOF를 알 수가 없음
- 변수의 크기, 정렬, 채워넣기, 바이트 순서가 다르기 때문에 어떤 애플리케이션에서 기록한 바이너리 데이터를 다른 앱에서는 못 읽을 수도 있다.
정렬문제
- 모든 아키텍처는
데이터 정렬 요구사항
을 가지고 있음. - 프로세스는 바이트 크기 단위로 메모리를 읽고 쓰지 않고, 2,4,8,,, 바이트처럼 정해진 기본 단위로 메모리에 접근함. → 기본 단위의 정수배로 시작하는 주소에 접근해야함
- 따라서 C언어에서 변수는 반드시 정렬된 주소에 저장하고 접근해야함.
- 예를 들어 32비트 정수는 4바이트 경계에 맞춰 정렬됨. → int는 4로 나누어 떨어지는 메모리 주소 공간에 저장된다.
- 정렬되지 않은 데이터 접근에 대해서는 다양한 패널티가 존재한다.
- 접근 가능 but 성능 저하
- 접근 허용 X, 하드웨어 예외로 처리
- 강제 정렬을 위해 하위 비트를 제거해버림
3.7 스트림에 쓰기
3.7.1 한 번에 문자 하나만 기록하기
- fgetc()에 대응하는 쓰기 함수는
fputc()
이다.
1
| int fputc(int c, FILE *stream);
|
- c로 지정한 바이트를 (unsigned char로 변환한 후에) stream이 가리키는 스트림에 쓴다.
- 문자 혹은 숫자가 아스키 코드표에 맞게 int값으로 들어감.
- 성공 시 c 반환, 실패 시 EOF 반환하고 errno 설정
1
| if (fputc ('p', stream) == EOF)
|
3.7.2 문자열 기록하기
1
| int fputs (const char *str, FILE *stream)
|
- str이 가리키는 NULL로 끝나는 문자열 전무를 stream이 가리키는 스트림에 기록한다.
- 성공하면 음수가 아닌 값 반환, 실패 시 EOF 반환
3.7.3 바이너리 데이터 기록하기
- C 변수처럼 바이너리 데이터를 직접 저장하려면 표준 입출력에서 제공하는
fwrite()
를 사용
1
2
3
4
| size_t fwrite (void *buf,
size_t size,
size_t nr,
FILE *stream);
|
- buf가 가리키는 데이터에서 size크기의 엘리먼트 nr개를 stream에 쓴다.
3.8 사용자 버퍼 입출력 예제 프로그램
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
| int main(void)
{
FILE *in, *out;
struct pirate {
char name[100];
unsigned long booty;
unsigned int beard_len;
} p, blackbeard = {"Edward Teach", 950, 48};
out = fopen ("data", "w");
if (!out) {
perror("fopen");
return 1;
}
if (!fwrite(&blackbeard, sizeof(struct pirate), 1, out)){
perror ("fwrite");
return 1;
}
if (fclose(out)){
perror("fclose");
return 1;
}
in = fopen("data", "r");
if (!fread(&p, sizeof (struct pirate), 1, in)){
perror("fread")
return 1;
}
}
|
변수 크기, 정렬 등에서 차이가 있기 때문에 특정 애플리케이션에서 쓴 바이너리 데이터를 다른 애플리케이션에서 읽지 못할 수도 있다.
만약 unsigned long
타입의 크기가 바뀌거나 채워 넣는 값의 양이 달라진다면 정확한 데이터를 못쓸것. 아키텍처와 ABI가 동일한 경우에만 바이너리 데이터를 일관적으로 읽고 쓸 수 있음.
- ABI : Application Binary Interface
3.9 스트림 탐색하기
1
| int fseek (FILE *stream, long offset, int whence)
|
- offset과 whence에 따라 stream에서 파일 위치를 조작한다.
whence
- SEEK_SET - 파일 위치를 offset값으로 설정
- SEEK_CUR - 현재위치에서 offset만큼 더한 값으로 설정
- SEEK_END - 파일 위치를 파일 끝에서 offset만큼 더한 값으로 설정
- 성공하면 0 반환하고 EOF 지시자를 초기화하고 이전에 실행했던 ungetc()를 취소한다.
- 에러 발생하면 -1 반환하고 errno를 설정
- EBADF - 유효하지 않은 스트림
- EINVAL - whence인자 잘못됨
fsetpos
는 stream의 위치를 pos로 설정한다.
1
| int fsetpos (FILE *stream, fpos_t *pos)
|
- 이는 whence가 SEEK_SET인 fseek()와 동일하게 동작함.
- C의 long 타입만으로는 스트림의 위치를 지정하기에 충분하지 않으므로 어떤 플랫폼에서는 이 함수가 스트림 위치를 특정한 값으로 설정할 수 있는 유일한 방법임.
1
2
3
4
5
| void rewind (FILE *stream)
// ==
fseek(stream, 0, SEEK_SET);
|
- 스트림을 시작 위치로 되돌리며 fseek을 위와 같이 사용하는 것과 동일함.
- 하지만 fseek()와는 달리
rewind()
는 오류 지시자를 초기화 한다. - rewind는 반환값이 없어서 에러 조건을 직접적으로 파악할 수가 없음.
1
2
3
4
5
6
| // 이런식으로 직접 확인을 해야함.
errno = 0;
rewind(stream);
if (errno)
//error
|
3.9.1 현재 스트림 위치 알아내기
- lseek()와는 다르게 fseek()는 갱신된 위치를 반환하지 않음.
- 따라서 위치를 파악하기 위한 용도로 분리된 인터페이스를 제공함.
ftell
은 현재 스트림 위치를 반환한다.
1
| long ftell(FILE *stream);
|
- 표준 입출력에서는
fgetpos
도 제공을 한다.
1
| int fgetpos (FILE *stream, fpos_t *pos)
|
- 성공하면 0을 반환하고 현재 스트림 위치를 pos에 기록함.
- 실패하면 -1을 반환하고 errno를 설정
- fsetpos()와 마찬가지로 fgetpos()는 복잡한 파일 위치 타입을 사용하는 비-유닉스 플랫폼을 위해 제공한다.
3.10 스트림 비우기
- 표준 입출력 라이브러리는 사용자 버퍼를 커널로 비워서 스트림에 쓴 모든 데이터가 write()을 통해 실제로 디스크에 기록되도록 만드는 인터페이스를 제공함.
1
| int fflush (FILE *stream);
|
- stream에 있는 쓰지 않은 데이터를 커널로 비운다.
- stream이 NULL이면 프로세스의 열려있는 모든 입력 스트림이 비워짐.
- 성공하면 0 반환, 실패하면 EOF반환하고 errno를 설정
fflush()
와 버퍼- 여기서 설명하는 모든 함수 호출은
커널이 유지하는 버퍼
가 아니라 C 라이브러리가 관리하는 버퍼
를 의미한다. 이는 커널 영역이 아니라 사용자 영역에 위치함. → 시스템 콜을 사용하지 않고 사용자 코드를 실행함으로써 성능개선 - fflush()는 단지 사용자 버퍼에 있는 데이터를 커널 버퍼로 쓰기만 함. → 이는 사용자 버퍼를 사용하지 않고 write()을 직접 사용하는 효과와 동일
- 즉, 데이터를 매체에 물리적으로 기록한다는 보장이 없다.
- 데이터가 매체에 즉각 기록되어야 하는 경우에는 fflush()를 호출한 다음 바로 fsync()를 호출한다. → 사용자 버퍼를 커널에 쓰고 fsync()를 통해 커널 버퍼를 디스크에 기록하도록 보장한다.
3.11 에러와 EOF
1
| int ferror(FILE *stream)
|
1
| int feof (FILE *stream)
|
1
| void clearerr (FILE *stream);
|
- 반환값이 없고 항상 성공하기 때문에 stream 인자값이 정상인지 확인할 수 있는 방법이 없다.
- 이를 호출하고 나면 다시 복구할 방법이 없으므로
에러 지시자
와 EOF 지시자
를 먼저 검사한 후에 호출해야함
3.12 파일 디스크립터 얻어오기
- 스트림에서 파일 디스크립터를 구해야 하는 경우가 있다.
1
| int fileno (FILE *stream)
|
fileno
를 통해서 fd를 구할 수 있다.- 성공하면 stream과 관련된 fd를 반환하고, 실패하면 -1을 반환.
- 주어진 스트림이 유효하지 않은 경우 errno를 EBADF로 설정
표준 입출력 함수와 시스템 콜 사이에서 사용자 버퍼링과 관련된 충돌이 발생하지 않도록 주의해야 함 fd를 사용하기전에 스트림을 비우는 것은 좋은 습관. 어쨌든 두가지를 섞어 쓰는 것은 좋지 않다.
3.13 버퍼링 제어하기
- 표준 입출력은 세 가지 유형의 사용자 버퍼링을 구현하고, 버퍼의 유형과 크기를 다룰 수 있는 인터페이스를 제공한다.
- 각각의 사용자 버퍼링 타입은 저마다의 목적이 있으며 상황에 맞게 사용할 때 가장 이상적임
버퍼 미사용
- 사용자 버퍼를 사용하지 않는다.
- 즉, 커널로 바로 데이터를 보낸다. 표준 에러를 제외하고는 거의 사용되지 않음
행 버퍼
- 행 단위로 버퍼링을 수행한다.
- 즉, 개행문자가 나타나면 버퍼의 내용을 커널로 보난다.
- 화면 출력 메시지는 개행문자로 구분되기 때문에 행 버퍼는 화면 출력을 위한 스트림일 경우 유용함.
- 표준 출력처럼 터미널에 연결된 스트림에서 기본적으로 사용
블록 버퍼
1
| int setvbuf (FILE *stream, char *buf, int mode, size_t size);
|
mode
- _IONBF - 버퍼 미사용
- _IOLBF - 행 버퍼
- _IOFBF - 블록 버퍼
- buf 와 size를 무시하는 _IONBF를 제외하고 나머지는 size 바이트 크기의 버퍼를 가리키는 buf를 주어진 stream을 위한 버퍼로 사용한다.
- buf가 NULL이라면 glibc 가 자동적으로 지정된 크기만큼 메모리를 할당한다.
- 스트림을 연 다음 다른 연산을 수행하기 전에 호출해야함.
- 제공된 버퍼는 스트림이 닫힐 때까지 반드시 존재해야 한다.
- 흔히 스트림을 닫기 전에 끝나는 스코프 내부의 자동 변수로 버퍼를 선언하는 실수를 함.
- 특히 main()에서 지역변수로 버퍼를 만든 다음에 스트림을 명시적으로 닫지 않는 경우를 주의해야함.
1
2
3
4
5
6
7
8
9
| int main(void){
char buf[BUFSIZ];
// stdout을 bufsiz 크기에 맞춰 블록 버퍼로 설정한다.
setvbuf(stdout, buf, _IOFBF, BUFSIZ);
return 0;
// buf는 스코프를 벗어나고 해제된다. 하지만 stdout을 닫지 않았음
}
|
- 스코프를 벗어나기 전에 스트림을 명시적으로 닫아주거나, buf를 전역 변수로 설정함으로써 방지할 수 있음.
표준 에러를 제외하고 터미널은 행 버퍼링으로 동작, 파일은 블록 버퍼링을 사용하는 것이 맞다. 블록 버퍼링에서 버퍼의 기본 크기는 BUFSIZ이며 일반적인 블록 크기의 정수배인 최적의 값이다.
따라서 개발자들은 일반적으로 스트림을 다룰 때 버퍼링에 대해 고민할 필요가 없다.
3.14 스레드 세이프
스레드
: 개별 프로세스 내에 존재하는 여러 개의 실행 단위. 멀티 스레드 프로세스 : 주소 공간을 공유하는 여러 개의 프로세스
스레드 세이프
(Thread-safe) 멀티 스레드 프로그래밍에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻함
→ 멀티스레드 환경에서 동작해도 원래 의도한 대로 동작하는 것을 스레드 세이프
하다고 할 수 있음
Thread Safe 하지 않은 코드 예시
1
2
3
4
5
6
7
8
9
10
11
12
| int num;
boolean is_even;
int inc(int n)
{
num += n;
if ((num%2) == 0)
is_even = true;
else
is_evne = false;
return num;
}
|
- num이라는 변수에 숫자를 더해서 짝수이면 is_even = true, 홀수이면 false로 설정
- 싱글 스레드 환경에서는 문제없는 코드
- 위의 라인을 멀티스레드에서 수행했을 경우 의도와 맞지 않는 결과가 발생할 수 있음
- 멀티코어 시스템에서는 둘 이상의 스레드가 같은 프로세스에서 동시에 실행될 수가 있다.
- 스레드에서 데이터에 접근할 때
동기화에 주의
하지 않거나, 스레드 로컬
(스레드 감금) 로 만들지 않으면 스레드가 공유 데이터를 덮어써버릴 수 있다.
- 스레드를 지원하는 OS는 락 메커니즘을 지원한다. 표준 입출력은 이런 메커니즘을 활용해서 단일 프로세스 내의 여러 스레드가 동시에 표준 입출력을 호출할 수 있도록 한다. 심지어는 같은 스트림에 대해서도 가능하다.
- 하지만 이것만으로는 부족함
- 여러 함수 호출을 그룹으로 묶어 통째로 락을 걸면 크리티컬 섹션이 하나의 입출력 연산에서 여러 입출력 연산으로 확장됨
- 크리티컬 섹션 : 임계 구역. 다른 스레드의 간섭 없이 실행할 수 있는 코드
- 효율성을 높이기 위해 락을 완전히 없애고 싶은 경우
- 락을 없애면 온갖 문제가 난무하지만 어떤 프로그램은 모든 입출력을 싱글 스레드에 위임하여 스레드를 가두어서 스레드 세이프를 구현하기도 함. → 락에 의한 오버헤드가 없다.
- 스레드는 입출력 요청에 앞서 락을 획득하고 고유 스레드가 되어야 함
- 표준 입출력 함수는 본질적으로 스레드 세이프를 보장한다는 것
- 단일 함수 호출 관점에서 보면 표준 입출력 연산은 Atomic 하다.
3.14.1 수동으로 파일 락 걸기
- stream의 락이 해제될 때까지 기다린 후에 락 카운터를 올리고 락을 얻은 다음, 스레드가 stream을 소유하도록 만든 후에 반환한다.
1
| void flockfile (FILE *stream);
|
funlockfile
함수는 stream과 연관된 락 카운터를 하나 줄인다.
1
| void funlockfile(FILE *stream);
|
- 락 카운터가 0이 되면 현재 스레드는 stream의 소유권을 포기해서 다른 스레드가 락을 얻을 수 있도록 한다.
여러번 중첩 호출이 가능함.
ftrylockfile()
함수는 flockfile()의 논블록 버전이다.
1
| int ftrylockfile (FILE *stream)
|
- stream이 락이 걸려있다면
ftrylockfile()
은 아무것도 하지 않고 즉시 0이 아닌 값을 반환함. - 논블록이 아니라면, 락이 걸린 상태에서는 블록되어서 계속 기다려야함.
- 만약 stream이 락이 걸린 상태가 아니라면 락을 걸고 락 카운터를 하나 올린 다음 그 stream을 소유하도록 만들고 0을 반환한다.
1
2
3
4
5
6
7
| flockfile (stream);
fputs("a", stream);
fputs("b", stream);
fputs("c", stream);
funlockfile(stream);
|
- 기록 중 다른 스레드가 중간에 끼어들지 못하게 하려면 락을 이용해야함.
- 설계 자체에서 동일 스트림을 대상으로 입출력하지 않도록 해야한다.
- 만약 그렇게 할 수 없다면 flockfile() 같은 함수를 이용해서 크리티컬 섹션을 확장해야 함.
3.14.2 락을 사용하지 않는 스트림 연산
- 상세하고 정밀한 락 제어를 통해 가능한 한 락 오버헤드를 최소화해서 성능을 향상시킬 수 있기때문에 스트림에 대해 수동으로 락을 설정함.
- 앞서 다루었던 표준 입출력 함수들은 내부적으로 락을 사용한다.
- 수동으로 락을 걸면 각 표준 입출력 함수들의 내부적인 락을 사용하지 않아도 된다.
_unlocked
postfix가 붙은 함수들을 이용하면 락 오버헤드를 최소화할 수 있다.
3.15 표준 입출력 비평
- 몇몇 전문가는 표준 입출력의 결함을 지적함
- fgets()는 충분한 기능을 제공하지 못한다
- wild character 같은 경우 eof 로 읽을 수도 있음
- gets()는 표준에서 제거되기도 함..
1
| 경고: the ‘gets‘ function is dangerous and should not be used.
|
이 문제는 gets함수가 strName[20]의 크기를 모르면서 개행을 찾거나 EOF를 만날 때까지 계속 읽기 때문에 주어진 버퍼의 크기를 넘을 수 있습니다. 다르게 설명하자면 C언어는 경계 검사를 수행하지 않아 gets함수가 접근 권한이 없는 주소에 도달 할 때까지 읽기를 계속합니다. 접근 권한이 없는 주소에 도달하는 이런 행위가 아마도 Linux에서는 시스템을 이상 종료 시킬 수 있는 오류 일 것입니다. 그래서 많은 바이러스가 이러한 문제점을 이용합니다.
- 가장 큰 불만은
이중 복사로 인한 성능 문제
이다.- 데이터를 읽을 때 표준 입출력의 read() 시스템 콜을 사용하면 데이터는 커널에서 표준 입출력의 버퍼로 복사된다.
- fgetc()같은 표준 입출력을 통해서 읽기를 요청하면 그 데이터는 표준 입출력 버퍼에서 인자로 제공된 버퍼로 또 복사된다.
- 쓰기 요청도 마찬가지
- 읽기 요청은 표준 입출력 버퍼를 가리키는 포인터를 반환하는 대체 구현으로 이중 복사 문제를 피할 수 있음
- 쓰기 요청도 포인터 기록을 통해서 피할 수 있음
3.16 맺음말
- 표준 입출력은 표준 C 라이브러리의 일부로 제공되는 사용자 버퍼링 라이브러리이다.
- 아래의 가정을 만족할 때 표준 입출력과 사용자 버퍼링은 의미가 있다.
- 많은 시스템 콜이 의심되는 경우 수 많은 호출을 합쳐서 줄이는 방법으로 오버헤드를 줄이고 싶다.
- 성능이 중요하며 모든 입출력은 정렬된 블록 경계에 맞춰 블록 크기 단위로 일어나도록 확실하게 보장해야 한다.
- 접근 패턴이 문자나 행 기반이며 낯선 시스템 콜에 의지하지 않고 손쉽게 데이터에 접근할 수 있는 인터페이스가 필요하다.
- 저수준 리눅스 시스템 콜보다는 고수준의 인터페이스를 선호한다.