스트림 관련 시스템 콜

리눅스의 입출력은 거의 4가지 시스템 콜로 처리된다.

시스템 콜이란? 응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스 이다.

  • 스트림에서 바이트 열을 읽는 read
  • 스트림에 바이트 열을 쓰는 write
  • 새로운 스트림을 생성하는 open
  • 사용 완료한 스트림을 닫는 close

파일 디스크립터

프로세스에서 파일을 읽거나 쓸 때, 다른 프로세스와 데이터를 주고 받을 때 스트림을 사용한다.

우리가 만든 프로그램에서 스트림을 사용할려면 파일 디스크립터를 사용해야한다.

파일 디스크립터는 커널이 스트림을 열 때 부여하는 번호이다.

표준 입력/표준 출력/표준 에러 출력

흔히 셸을 통해 코딩을 할때 표준입력, 표준 출력, 표준 에러 출력에 대한 스트림은 기본적으로 생성되며 이에 대한 디스크립터값 또한 미리 할당해준다.

각 순서에 맞게 0,1,2로 대응되며 매크로도 따로 존재한다.
STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO

동작 과저에 대해 간단한 예로 설명한다면 cat명령어를 통해 실행인자가 main.c인 상황.

표준 입출력이기 때문에 이미 해당 스트림은 생성되어 있고 디스크립터값 또한 할당되어 있다.

main.c을 표준 입력으로 cat명령 프로세스를 통고하여 다시 표준 출력으로 단말(디스플레이)에 표시된다.

스트림 읽기 및 쓰기

읽고 쓰기 말 그대로 read, write 시스템 콜을 사용한다.

man 2 read으로 메뉴얼을 보면 자세한 시스템 콜 read에 대한 설명을 알 수 있다.

ssize_t는 부호가 있는 정수형, size_t는 부호가 없는 정수형 이는 별명에 지나지 않으며 운영체제와 커널의 버전에 따라 동일한 소스코드를 사용하기 위함이다.

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t bufsize);

read는 fd의 디스크립터 번호에 해당하는 스트림에서 바이트 열을 읽는 시스템 콜이다.

최대 bufsize만큼 바이트 수를 읽어서 buf에 기록한다.(buf크기는 bufsize로 할당하는 것이 일반적)

read가 올바르게 읽기 작업이 완료되면 읽어들인 바이트 수를 반환하고 끝에 도달한 경우에는 0을 중간에 에러가 발생한 경우는 -1을 반환한다.

  • read는 읽어들인 데이터 끝에 ‘\0’문자가 있다고 전제하지 않는 API이기 때문에 읽어 들인 문자열의 끝에 ‘\0’이 들어가 있다고 생각하면 안된다.

write는 스트림에 바이트 열을 쓸 때 사용한다.

#include<unistd.h>

ssize_t write(int fd, const *buf, size_t bufsize);

bufsize바이트만큼 buf의 내용을 fd로 지정한 파일 디스크립터의 스트림에 쓴다.

정상적으로 쓴바이트 수를 반환하거나 에러의 경우에는 -1을 반환한다.

스트림 생성

파일을 읽고 쓰는 스트림을 생성할려면 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);

path로 지정한 경로의 파일에 대한 스트림을 만들고 반환값을 해당 스트림을 가리키는 파일 디스크립터를 반환한다.

flag
O_RDONLY 읽기 전용
O_WRONLY 쓰기 전용
O_RDWD 일고 쓰기
O_CREATE 파일이 존재하지 않으면 새롭게 만든다.
O_EXCL O_CREAT와 함꼐 사용되어 이미 파일이 존재하면 에러가 된다.
O_TRUNC O_CREAT와 함께 사용되어 이미 파일이 존재하면 파일의 크기를 0으로 만든다.
O_APPEND write()함수가 항상 파일의 끝에 쓰도록 설정한다.

사용이 끝난 스트림은 close()로 닫는다.

#include <unistd.h>

int close(int fd);

파일 디스크립터 fd에 연결된 스트림을 해제한다.

오류가 없이 닫히면 0, 에러가 발생하면 -1을 반환한다.

close()함수 예시

if (close(fd) < 0){
  // 에러처리
}

사실 스트림은 프로세스가 종료되면 커널이 파기하기 때문에 close하지 않아도 문제가 생기지 않지만 명시적으로 닫아주는게 좋다.

cat 명령어

cat은 concatenate(연결하다)라는 단어에서 유래되었다고 한다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

static void do_cat(const char *path);
static void die(const char *s);

int
main(int argc, char *argv[])
{
 int i;
 if(argc < 2){
  fprintf(stderr, "%s: file name not given\n", argv[0]);
  exit(1);
 }
 for(i = 1; i < argc; i++){
  do_cat(argv[i]);
 }
 exit(0);
}

#define BUFFER_SIZE 2048

static void do_cat(const char *path)
{
 int fd;
 unsigned char buf[BUFFER_SIZE];
 int n;

 fd = open(path, O_RDONLY);
 if(fd < 0) die(path);
 for (;;){
  n = read(fd, buf, sizeof buf);
  if(n < 0) die(path);
  if(n == 0) break;
  if(write(STDOUT_FILENO, buf, n) < 0) die(path);
 }
 if(close(fd) < 0) die(path);
}

static void
die(const char *s)
{
 perror(s);
 exit(1);
}

cat기능을 단순화하여 구현한 코드이다.

include되어 있는 헤더파일들은 작성할때 함수에 맞는 헤더파일을 진행하며 추가한다. man으로 함수를 검색하여 필요한 헤더파일을 추가한다.

static캡슐화 목적으로 cat.c의 내부함수로만 목적으로 사용한다.

if(argc < 2)으로 명령어를 제외한 인자값이 들어오는지 안들어오는지 체크한다.

문제가 없으면 들어온 파일 명을 하나씩 do_cat함수로 넘겨서 반복한다.

open으로 스트림을 생성후 디스크립터를 반환받고 -1을 반환한다면 종료한다.

문제가 없다면 버퍼 사이즈만큼 read하며 buf에 담고 길이를 반환한다.

길이(n)이 -1이라면 오류라고 판단하고 종료하고 0이라면 끝까지 다읽은 경우라 종료한다.

write로 buf에 들어있는 정보를 출력한다. (STDOUT_FILENO이기 때문에 읽기 전용.)

++ 일반적으로 시스템콜이 실패할 경우 errno이라는 전역변수가 자동으로 설정된다.

이러한 errno에 맞는 에러메세지를 출력하는 것이 perror(3)(3이기 때문에 표준라이브러리)

파일 오프셋

스트림이 파일에 연결되어 있다는 의미는 같은 파일 디스크립터에 대해 read()를 반복해서 호출하면 마지막에 도달하게 되는데(bufsize에 따라 반복횟수의 차이)이것은 스트림이 마지막으로 읽은 파일의 위치를 기억하기 때문이다.

스트림은 파일의 특정 위치에 연결되어 있다. 이렇게 스트림에 연결되어 있는 위치를 파일 오프셋이라고 한다.

이러한 파일오프셋은 스트림의 속성으로 시스템콜을 사용하여 조작할 수 있다.


모두를 위한 리눅스 프로그래밍책을 기준으로 작성하였습니다.

댓글남기기