스트림과 관련된 시스템 콜
- 스트림에서 바이트 열을 읽는
read
- 스트림에서 바이트 열을 쓰는
write
- 새로운 스트림을 생성하는
open
- 사용 완료한 스트림을 닫는
close
파일 디스크립터
-
프로세스에서 파일을 읽거나, 쓸 때 혹은 다른 프로세스와 데이터를 주고 받을 때 스트림을 사용한다고 했다. 그렇다면, 우리가 만든 프로그램에서 스트림을 사용하려면 어떻게 해야할까?
-
이를 위해서 파일 리스크립터(file desriptor) 라는 것을 사용하는데, 커널이 스트림을 열 때 부여하는 번호이다.
-
커널이 만들어준 스트림의 번호(파일 디스크립터)를 알아야 스트림을 본격적으로 사용할 수 있다.
표준 입력, 표준 출력, 표준 에러 출력
-
보통 셸을 통해서 프로세스가 생성되는 경우, 세 개의 스트림이 기본으로 생성되며, 이에 대한 파일 디스크립터 값이 미리 할당된다.
-
이 새 개의 스트림이란 표준 입력(standard input), 표준 출력(standard output), 표준 에러 출력(standard error output) 이다.
-
이들 스트림은 각각 파일 디스크립터 0번, 1번, 2번에 할당된다. 이 값에 대한 매크로도 준비되어 있는데 각각
STDIN_FILENO
,STDOUT_FILENO
,STDERR_FILENO
이다.
표준 입력과 표준 출력
-
표준 입력과 표준 출력을 잘 활용하면 리눅스 명령어를 조합하여 고도의 작업을 수행할 수 있다.
-
여러 명령어를 파이프로 연결하여 데이터를 처리하는 것이 가능한 것도 각 명령어가 표준 입력에서 데이터를 읽어들이고, 처리 결과를 표준 출력에 쓰게 되어 있기 때문이다.
-
이 경우의 ‘표준’은 ‘디폴트’에 가까운 의미이다. 표준 입력은 프로그램의 디폴트 입력 소스이며, 표준 출력은 디폴트 출력지라는 의미이다.
-
여러개의 리눅스 명령어를 조합할 수 있는 것도 각 리눅스 명령어가 표준 입력에서 데이터를 읽고, 표준 출력에 쓰는 약속을 했기 때문이다.
표준 에러 출력
-
표준 에러 출력은, 에러 메시지를 출력하기 위해서 사용한다.
-
일반적으로 표준 출력은 파이프라인을 통해서 다른 프로그램의 표준 입력에 연결된다. 따라서 에러 메시지를 표준 출력으로 내보내면 사람이 에러 발생을 알기 어렵게 된다.
-
그래서 여분의 스트림을 준비해서 일반 메시지는 파이프라인을 통해 표준 출력으로 내보내고, 에러 메시지는 표준 에러 출력으로 출력하도록 한 것이다.
파일 디스크립터 | 매크로 | 의미 |
---|---|---|
0 | STDIN_FILENO |
표준입력, 기본 입력 스트림 |
1 | STDOUT_FILENO |
표준 출력, 기본 출력 스트림 |
2 | STDERR_FILENO |
표준 에러 출력, 별도 메시지 출력용 |
스트림 읽기 및 쓰기
-
스트림에서 데이터를 읽고 쓰기 위해서
read()
,write()
라는 시스템 콜을 사용한다. -
스트림에서 바이트 열을 읽기 위해서 사용하는 시스템 콜이
read()
이다.
read(2)
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t bufsize);
-
read()
는 파일 디스크립터 번호인fd
에 해당하는 스트림에서 바이트 열을 읽는 시스템 콜이다. -
bufsize
로 바이트 수를 읽어서buf
에 기록한다.buf
의 크기는bufsize
로 할당하는 것이 일반적이다. -
read()
는 읽기 작업이 완료되면 읽어들인 바이트 수를 반환한다. 그리고 파일의 끝에 도달한 경우에는 0을 반환하고, 중간에 에러가 발생한 경우에는 -1을 반환한다. -
bufsize
바이트 수 보다 적은 바이트를 읽는 경우도 많으므로 반환값을 체크하도록 코딩해야한다.
주의사항
C언어 문자열에는 임의의 바이트 열을 저장할 수 있지만, 일반적으로 문자열의
끝에는 '\0'을 넣는 것이 관례이다. API 중에도 문자열의 끝에 '\0'이 있다고
전체하는 것과 그렇지 않은 것이 있어서 사용에 주의 해야한다.
예를 들어, 문자열의 끝에 '\0'이 있다고 전제하는 API에 '\0'로 끝나지
않는 문자열을 전달하면 문제가 될 수 있다.
-
read
의 경우에는 읽어들인 데이터의 끝에\0
가 있다고 전제하지 않는API
이다. 따라서read(2)
를 통해서 읽어 들인 문자열의 끝에\0
이 있다고 생각하고 코드를 작성해서는 안된다. -
예를 들어,
printf()
의 경우에는 문자열의 끝에 ‘\0’이 들어가 있다고 전제하는API
이므로,read(2)
로 읽은 문자열을 그대로printf()
로 전달하면 안된다. 이는 보안상의 취약점이 될 수 있다.
write(2)
- 스트림에 바이트 열을 쓸 때는 시스템 콜
write()
를 사용한다.
#include <unistd.h>
ssize_t write(int fd, const *buf, size_t bufsize);
-
write()
는 인자로 지정한bufsize
바이트만큼,buf
의 내용을fd
로 지정한 파일 디스크립터의 스트림에 쓴다. 반환값의 데이터 타입인ssize_t
는 부호 있는 정수를 의미한다. -
정상적으로 쓴 바이트 수를 반환하고 에러가 발생한 경우에는
-1
를 반환한다. -
write()
가bufsize
로 지정한 바이트 수만큼 쓸 수 없는 상황은 비교적 드물지만, 발생한 여지는 충분히 있다. 정확한 처리가 필요한 경우에는 반환값을 체크하도록 하자.
스트림이란?
-
스트림은 파일 디스크립터로 표현되고,
read()
나write()
를 통해서 읽고 쓸 수 있다. 예를 들면, 파일을open()
하면read()
나write()
를 통해서 파일의 내용을 읽거나 쓸 수 있는데, 이것이 바로 스트림이다. -
같은 맥락에서 파이프라인이나, 소켓도 스트림으로 볼 수 있다.
파일 열기
프로세스가 만들어 질 때부터 사용할 수 있는 스트림으로는 표준 입력, 표준 출력, 표준 에러 출력이 있다. 이번에는 이 이외의 스트림을 만드는 방법에 대해서 알아보자.
open(2)
- 파일을 읽고 쓰는 스트림을 만들려면 시스템 콜
open()
을 사용한다.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int flags);
int open(const char *path, int flags, mode_t mode);
-
open()
은 첫 번째 인자path
로 지정한 경로의 파일에 대한 스트림을 만들고, 그 스트림을 가리키는 파일 디스크립터를 반환한다. 이러한 과정을 흔히 파일을 연다라고 한다. -
두 번째 인자인
flags
에서는 파일을 어떤 모드로 열 것인지 비트 연산자를 사용하여 지정한다.
파일을 열 때 사용할 수 있는 flag(1)
flag | 의미 |
---|---|
O_RDONLY |
읽기 전용 |
O_WRONLY |
쓰기 전용 |
O_RDWR |
읽고 쓰기 |
close(2)
- 사용이 끝난 스트림은
close()
로 닫는다.
#include <unistd.h>
int close(int fd);
-
close
는 파일 디스크립터fd
에 연결된 스트림을 해제한다. 이러한 처리를 파일을 닫는다고 한다. -
오류 없이 닫히면
0
, 에러가 발생하면-1
를 반환한다. 일반적으로close()
함수를 호출하는 코드는 다음과 같다.
if (close(fd) < 0) {
/* 에러 처리 */
}
-
프로세스가 종료되면 사용하던 모든 스트림을 커널이 파기 하기 때문에,
close()
를 하지 않아도 시스템에 이상이 생기지 않을 수 있다. -
그러나 사용이 완료된 스트림은 반드시 종료시켜주는 것이 좋다. 프로세스가 동시에 사용할 수 있는 스트림의 개수에 제한이 있기도 하고, 스트림의 반대편에 프로세스가
close()
할 대까지 기다리고 있을 수도 있다. 모든 리소스는 사용이 완료되었을 때 닫아주는 것이 바람직하다.