리눅스 프로그래밍 - 3주차

Lellow_Mellow·2022년 10월 11일
1
post-thumbnail

🔔 학교 강의를 바탕으로 개인적인 공부를 위해 정리한 글입니다. 혹여나 틀린 부분이 있다면 지적해주시면 감사드리겠습니다.

System Call : write

#include <unistd.h>
ssize_t write(int filedes, const void *buffer, size_t n);

argument

  • int filedes : file descriptor
  • const void *buffer : 쓰일 data의 pointer
  • size_t n : 쓰일 byte 수

return

  • 성공 시, write한 byte 수를 반환
  • error 발생 시, -1 return

buffer 안에 있는 내용을 파일에 n만큼 write하는 system call이다. 기존에 존재하는 파일을 write하기 위해 open한 경우, char 단위로 덮어쓰게 된다. (커서가 처음에 있기 때문)

O_APPEND option을 이용하면, file positionend of file로 이동하여 덮어쓰지 않고 파일의 마지막부터 이어서 write이 가능하다.

이에 대한 예시 코드는 아래와 같다.

int copyfile (const char *name1, const char *name2) {
	int infile, outfile;
	ssize_t nread;
	char buffer[BUFSIZE];
    
	if ( (infile = open (name1, O_RDONLY ) )== -1)
		return(-1);
	if ( (outfile = open (name2, O_WRONLY | O_CREAT | O_TRUNC, 0644) )== -1) {
		close (infile);
		return (-2);
	}
	while ( (nread = read (infile, buffer, BUFSIZE) ) > 0) {
		if ( write(outfile, buffer, nread) < nread ) {
			close (infile);
			close (outfile);
			return (-3);
		}
	}
	close (infile);
	close (outfile);
	if ( nread == -1) return (-4)
	else return (0);
}

위의 코드에서 BUFSIZE = 512 이다. file name 2개를 인자로 받아 infile은 read only로 open, outfile은 write only로 open한다. 이때, 파일이 존재하지 않을 경우 create하고, 존재할 경우 file을 비워주도록 O_CREATE, O_TRUNC option을 추가해주었다.

이후에 infile을 BUFSIZE 만큼 read한 이후, outfile에 write을 반복한다. nread보다 write한 size가 작을 경우, 이는 error가 발생한 것이므로 infile과 outfile을 닫고 종료한다.

이후에 nread == -1 을 통해 최종적인 nread error를 catch한다.

read, write and efficiency

read, write kernel이 소비한 시간은 buffer size에 따라 달라진다. system call은 coast가 높기 때문에 buffer size가 작으면 system call의 횟수가 증가하게 된다. 따라서 system call을 줄이기 위해서는 buffer size를 늘릴 필요가 있다.

하지만 무작정 buffer size를 증가시키면 메모리 면에서 비효율적이며, 메모리를 낭비하게 된다. 1byte만 읽어온다고 하더라도 disk에서는 block 단위로 읽어와 쓰고 버리기 때문에, 근래 PC의 평균적인 block 단위인 4096이 가장 효율적이다. 하지만 이 값은 항상 절대적이지 않으며, 용도에 맞게 설정하는 것이 중요하다.

또한 read보다 상대적으로 write의 속도가 더 빠른 편이다. 이는 read는 값을 읽어 당장 반영해야 하지만, write는 파일이닫히거나 재접근 전까지만 task를 완료하면 되기 때문에 disk에 바로 전달하지 않고 buffer에 모아서 전달하기 때문이다. 따라서 write가 온전히 일어나기 전까지 process를 유지해야 write의 결과가 반영된다.

System Call : lseek

#inlcude <unistd.h>
off_t lseek(int filedes, off_t offset, int start_flag);

argument

  • int filedes : file descriptor
  • off_t offset : start flag로부터 옮길 정도
  • int start_flag : 시작 지점 지정
    - SEEK_SET #0 : 파일 시작 지점
    • SEEK_CUR #0 : 현재 지점
    • SEEK_END #2 : 파일의 끝

return

  • 성공 시, 새로운 file position을 return
  • error 발생 시, -1 return

커서, 다시 말해 file position을 특정 위치로 이동시켜주는 system call이다.

fd = open(fname, O_RDWR);
lseek(fd, (off_t)0, SEEK_END);
write(fd, outbuf, OBSIZE);

fd = open(fname, O_WRONLY|O_APPEND);
write(fd, outbuf, OBSIZE);

위와 아래 코드는 완전히 동일한 역할을 한다. 위치를 lseek를 이용하여 파일의 마지막으로 보낸 이후에 해당 위치에서 write를 진행하는 방식과, O_APPEND option을 이용하여 파일의 마지막부터 write를 시작하는 것은 결과가 동일하다.

off_t filesize;
int filedes;
filesize = lseek(fd, (off_t)0, SEEK_END);

또한 위처럼 마지막부터 처음으로 커서를 이동하여 파일의 크기를 알아낼 수 있다.

File Share

모든 process는 file descriptor table을 가지고 있으며, File descriptor flagfile table을 가리키는 포인터로 이루어져 있다. 각각의 file table은 file status flag, file position, v-node table을 가리키는 포인터로 이루어져 있다. 그림으로 보면 아래와 같다.

file talble은 file을 열때마다 생성되며, v-node table은 system에서 열려있는 file을 관리하는 구조체하나만 존재한다. i-node table에는 실제 파일에 대한 정보가 존재한다.

int fd3, fd4; char buf[20];
fd3 = open(“file”, O_RDWR);
fd4 = open(“file”, O_RDONLY);
read(fd3, buf, 20);
read(fd4, buf, 30);
close(fd3); close(fd4);

위의 코드의 실행 과정을 그림으로 나타내면 아래와 같으며, fd마다 file table은 따로 관리된다. 하지만 동일한 파일에 대한 v-node table의 경우는 단 하나만 존재하여 동일한 v-node table을 참조하는 것을 볼 수 있다.

다른 파일에서 각 코드를 실행하더라도 v-node table이 하나임은 변함없다.

System Call : dup, dup2

#include <unistd.h>
int dup(int filedes);
int dup2(int filedes, int filedes2);

dupdup2 둘 다 동일하게 기존의 file descriptor를 복제하여 새 file descriptor를 반환한다.

argument

  • int filedes : file descriptor
  • int filedes2 : 복제한 새로운 file descriptor를 저장

return

  • 성공 시, 새로운 file descriptor를 return
  • error 발생 시, -1 return

이에 대한 예시 코드는 아래와 같다.

newfd = dup(1)

위는 1번 file descriptor와 동일한 새로운 file descriptor를 생성하여 return해주는 코드이며, 이를 그림으로 나타내면 아래와 같다.

fd3 = open(“test”, O_RDWR);
dup2(fd3, 1);

dup2의 경우, 기존에 존재하는 file descriptor에 원하는 file descriptor를 복제하는 redirection이 가능하다. 기존의 file descriptor를 다시 사용하고 싶은 경우에는 dup2 이전에 기존의 file descriptor를 저장해두고, 이를 이용하여 다시 지정해줘야 한다.

System Call : fcntl

#include <fcntl.h>
int fcntl(int filedes, int cmd, ...);

file control의 약자로, 이미 열려있는 file에 대한 특성을 바꿀 수 있는 system call이다.

argument

  • int filedes : file descriptor
  • int cmd
    - F_DUPFD : dup과 동일한 역할을 수행
    - F_GETFL/F_SETFL : file status flag를 가져오거나 저장
    -> 다시말해 file에 어떻게 열렸는지에 대한 정보를 get/set

return

  • 성공 시, 새로운 file descriptor를 return
  • error 발생 시, -1 return

이에 대한 예시 코드는 아래와 같다.

#include <fcntl.h>

int filestatus(int filedes){
	int arg1;
	if (( arg1 = fcntl (filedes, F_GETFL)) == -1) {
		printf ("filestatus failed\n");
		return (-1);
	}

	switch ( arg1 & O_ACCMODE) {
		case O_WRONLY: printf ("write-only"); break;
		case O_RDWR: printf ("read-write"); break;
		case O_RDONLY: printf ("read-only"); break;
		default: printf("No such mode");
	}
	if (arg1 & O_APPEND)
		printf (" -append flag set");
        
	printf ("\n");
	return (0);
}

fcntl 에서 F_GETFL을 이용하여 file status flag를 get한 후, 이를 O_ACCMODE& 연산을 하여 file access mode를 확인하는 코드이다. O_ACCMODE는 하위 2bit만 1로 이루어져 있어, readwrite에 대한 정보만 얻을 수 있다.

2.2 Standard Input/Output/Error

Basic Concepts

File descriptor 0 : standard input
File descriptor 1 : standard output
File descriptor 2 : standard error

3개 모두 I/O 디바이스에 할당된다. 0은 display에 할당되며, 1, 2는 display에 할당된다.

Redirection

$ prog_name < infile

newfd = open(“infile”, O_RDONLY);
dup2(newfd, 0);

위의 첫 줄에 해당하는 코드는 아래 2줄의 코드와 의미가 같으며, infile을 standard input으로 사용하겠다는 의미이다.

$ prog_name > outfile

newfd = open(“infile”, O_WRONLY);
dup2(newfd, 1);

마찬가지로 위의 코드는 outfile을 standard output으로 사용하겠다는 의미이며, redirection하는 것과 동일하다.

$ prog_name < infile > outfile

또한 위와 같이 한꺼번에 지정하는 것도 가능하다.

$ prog_1 | prog_2

prog_1의 output을 prog_2의 input으로 지정하는 것도 가능하다. (PIPE 개념)

Standard input/output

#include <stdlib.h>
#include <unistd.h>
#define SIZE 512

int main() {
	ssize_t nread;
	char buf[SIZE];
    
	while ( (nread = read (0, buf, SIZE)) > 0)
		write (1, buf, nread);
	return 0;
}

buffer size를 512로 지정하고 0번 file descriptor를 이용하여 입력을 read하면 512가가득 찰때까지 기다리는 것이 아닌 줄바꿈, 다시 말해 return key가 눌릴 때마다 각 줄이 file descriptor 1번을 이용하여 write된다.

2.3 The Standard I/O Library : a look ahead

The Standard I/O Library

Standard I/OUnix I/O system call을 기반으로 만들어졌으며, Unix I/O system call는 단순히 시퀀스 형태의 byte data만 처리한다. 이에 비해 Standard I/Oauto buffering을 지원하며, 사용자 친화적이다.

Unix I/Ofile descriptor로 표현되지만, Standard I/OFILE*로 file을 표현한다.

#include <stdio.h>
#include <stdlib.h>

int main() {
	FILE *stream;
	if ( ( stream = fopen ("junk", "r")) == NULL) {
		printf ("Could not open file junk\n");
		exit (1);
	}
	return 0;
}

C standard library는 다양한 higher level의 standard I/O function들을 포함하며, 이는 아래와 같다.

Standard I/O Libary : fopen

#include <stdio.h>
FILE *fopen(const char *restrict pathname, const char *restrict type);

argument

  • const char *restrict pathname : 파일 이름
  • const char *restrict type
    - r or rb : read를 위해 open
    - w or wb : write를 위해 open (파일이 존재하면 파일을 비우고, 없으면 create)
    - a or ab : write를 위해 open (파일이 존재하면 파일의 끝에서 추가, 없으면 create)
    - r+ or r+b or rb+ : read와 write를 위해 open
    - w+ or w+b or wb+ : read와 write를 위해 open (파일이 존재하면 파일을 비우고, 없으면 create)
    - a+ or a+b or ab+ : read와 write를 위해 open (파일이 존재하면 파일의 끝에서 추가, 없으면 create)
    -> b가 붙은 경우 binary mode로 실행

Standard I/O Library : getc, putc

#include <stdio.h>
int getc(FILE *istream);
int putc(int c, FILE *ostream);

getc는 char 1개를 read한다. 성공할 경우 다음 char 1개를 get하여 return, end of file이거나 error일 경우 EOF를 return한다.

putc는 char 1개를 write한다. 성공할 경우 c를 return하고, error 발생 시 EOF를 return한다.

auto buffering이므로 하나만 읽어온다고 하더라도 block 단위로 내부적으로 읽어와 버리지 않고 buffer에 저장 후에 활용하므로 비효율적이지 않다.

Buffering

기본적으로 사용되는 버퍼는 일반적으로 stream에서 I/O가 처음 수행될 때, malloc을 호출하는 표준 I/O 함수 중 하나에 의해 얻는다.

Standard I/O Library : fprintf

#include <stdio.h>
int fprintf(FILE *restrict fp, const char *restrict format, ...);

fprintf는 성공 시에 출력한 문자 수를 return, error일 경우에는 음수를 return한다. 이에 대한 예시는 아래와 같다.

#include <stdio.h>
#include <stdlib.h>

int notfound (const char *progname, const char *filename) {
	fprintf (stderr, "%s: file %s not found\n", progname, filename);
	exit (1);
}

format에는 특정한 포맷을 지정하여 파일로 write할 수 있다.

2.4 The errno variable and system calls

Error Handling

system call이 -1을 return했을 경우, UNIX는 errno에 error code를 저장한다. (상수이며 이에 대한 error 내용은 E로 시작함) 가장 최근에 발생한 error에 대해서만 저장하고 있으며, reset되지 않는다.

이에 대한 예시는 아래와 같다.

#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

int main(int argc, char** argv) {
	int fd;
	if ( (fd = open ("nonesuch", O_RDONLY)) == -1) {
		fprintf(stderr, “error %d\n", errno);
		exit (1);
	}
	return 0;
}

nonesuch file이 존재하지 않으면 errno2 (ENOENT)로 지정되고, 권한이 없을 경우에는 3 (EACCESS)로 지정되며, fprintf 대신에 perror를 사용할 수 있다.

perror(“error opening nonesuch”);

// error opening nonesuch: No such file or directory
// -> 위와 같이 출력된다.
profile
festina lenta

0개의 댓글