42seoul:: pipex - linux pipe 구현

jahlee·2023년 3월 17일
0

개인 공부

목록 보기
9/23

사용 가능 함수

#include <fcntl.h>

- int open (const char *name, int flags, mode_t mode);

const char* name : 파일의 절대 경로 혹은 상대 경로이거나 파일 이름이다.
int flags : 반드시 O_RDONLY , O_WRONLY, O_RDWR 값들 중 하나이어야 한다. 각각 "읽기 전용", "쓰기 전용", "읽기 쓰기"를 나타낸다. 몇 가지 다른 값들과 or 연산을 통하여 같이 사용할 수 있다.
mode_t mode : flags에 O_CREAT가 포함되어있는 경우 새로 만들어지는 파일의 권한을 설정한다.

- int close (int fd);

#include <unistd.h>

- ssize_t read(int fd, void *buf, size_t count);
- ssize_t write(int fd, const void *buf, size_t count);
- int access(const char *pathname, int mode);

int mode 종류
R_OK : 파일 존재 여부, 읽기 권한 여부
W_OK : 파일 존재 여부, 쓰기 권한 여부
X_OK : 파일 존재 여부, 실행 권한 여부
F_OK : 파일 존재 여부

## 예시 ##
int main(void)
{
	char *pathname = "./hello.txt";
	int mode = R_OK | W_OK; // 비트 연산으로 여러가지를 한번에 체크 가능하다.
	if( access( pathname, mode ) == 0 )
		printf("읽고 쓸 수 있습니다.");
	else
		printf("권한이 없거나 존재하지 않습니다.");
}
- int dup(int fd);

dup는 fd로 전달받은 파일 서술자를 복제하여 반환합니다. dup가 돌려주는 파일 서술자는 가장 낮은 서술자를 반환합니다.
성공시 새 파일 서술자, 오류시 -1을 반환합니다.

- int dup2(int fd, int fd2);

dup2는 새 서술자의 값을 fd2로 지정합니다. 만일 fd2가 이미 열려있으면 fd2를 닫은 후 복제가 됩니다
성공시 새 파일 서술자, 오류시 -1을 반환합니다.

dup, dup2 예제 링크

- int execve(const char *filename, char *const argv[], char *const envp[]);

execve 는 다른 실행 파일을 실행 시키는 함수라 생각하면 된다.
execve(실행 파일, 실행파일 argv배열, 환경변수);

- pid_t fork(void);

현재 실행되는 프로세스에 대해 복사본 프로세스를 생성합니다. fork함수를 호출하는 프로세스가 부모 프로세스가 되고, 새롭게 생성되는 프로세스가 자식 프로세스가 된다. 자식 프로세스는 부모 프로세스의 메모리를 그대로 복사하여 가지게 된다. 그리고 fork함수 호출 이후 코드부터는 각자의 메모리를 사용하여 실행 된다.

(리턴값)

성공시 : 부모 프로세스에서는 자식 프로세스의 PID값을 반환 받음,
자식 프로세스에서는 0 값을 반환 받음.
실패시 : 음수 값 -1 반환

- int pipe(int fd[2]);

파이프(Pipe)란 프로세스간 통신을 할때 사용하는 커뮤니케이션의 한 방법이다. 가장 오래된 UNIX 시스템의 IPC로 모든 유닉스 시스템이 제공한다. 하지만 두가지 정도의 한계점이 있다.
첫번째 한계점으로 파이프는 기본적으로 반이중 방식이다. 물론 전이중 방식을 지원하는 시스템이 있긴 하나, 최대의 이식성을 위해서는 파이프는 반이중 방식이라는 생각을 해야한다. 이것은 FIFO라는 명명된 파이프로 극복할 수 있다.
두번째 한계점으로는 부모, 자식 관계에서의 프로세스들만 사용할 수 있다는 점이다. 부모프로세스가 파이프를 생성하고, 이후 자식 프로세스와 부모프로세스가 파이프를 이용하여 통신한다.
이러한 한계점이 잇긴 하지만 여전히 쓸모있는 IPC기법이다.

(리턴값)

성공 : 0
실패 : -1

- int unlink(const char *pathname);

파일을 삭제하는 system call 함수이다. 정확하게는 unlink는 hard link의 이름을 삭제하고 hard link가 참조하는 count를 1감소시킨다.
hard link의 참조 count가 0이 되면, 실제 파일의 내용이 저장되어 있는 disk space를 free하여 OS가 다른 파일을 위해서 사용할 수 있도록 한다. 따라서 hard link를 생성하지 않은 파일은 바로 disk space를 해제하여 사실상 파일 삭제한다.

참조

[리눅스] dup, dup2 설명 및 쉬운 사용법, 사용 예제(그림 포함)
[리눅스] 파이프(pipe) 개념과 예제

#include <stdlib.h>

- void* malloc(size_t size);
- void free(void *ptr);
- void exit(int status);

#include <string.h>

- char* strerror(int errnum);

errnum 의 값을 통해 발생하였던 오류에 알맞은 오류 메세지를 가리키는 포인터를 리턴한다. 이 때 리턴되는 포인터는 문자열 리터럴을 가리키고 있기 때문에 그 내용이 바뀔 수 없다.

#include <stdio.h>

- void perror(const char* str);

전역 변수 errno 의 값을 해석하여 이에 해당하는 시스템 오류 메세지를 표준 오류 출력 스트림(stderr)에 출력한다. 또한 추가적으로 전달하고자 하는 사용자 정의 메세지를 str 인자에 담아 출력할 수 도 있다.

#include <sys/wait.h>

- pid_t wait(int *statloc);

성공 : 프로세스 ID 반환
오류 : -1
부모 프로세스는 wait함수를 사용하여 자식 프로세스의 종료 상태를 얻을 수 있다.
다른 말로 wait함수를 사용하여 자식 프로세스가 종료 될때까지 기다릴 수 있다.
wait() 함수는 아래와 같이 동작합니다.
1. 자식 프로세스가 동작 중이면 호출 차단이 차단되기 때문에 상태를 얻어올 때까지 대기
2. wait() 함수 호출자가 시그널을 받을 때까지 대기
3. 자식 프로세스가 종료된 상태라면 즉시 호출이 반환되어 상태를 얻음,
이 때 wait() 함수는 자식 프로세스의 프로세스 ID를 반환
4. 자식 프로세스가 없다면 호출이 즉시 반환되며, 에러값을 반환

매크로 설명-
WIFEXITED(statloc) 자식 프로세스가 정상적으로 종료되었다면 TRUE
WIFSIGNALED(statloc) 자식 프로세스가 시그널에 의해 종료되었다면 TRUE
WIFSTOPPED(statloc) 자식 프로세스가 중단되었다면 TRUE
WEXITSTATUS(statloc) 자식 프로세스가 정상 종료되었을 때 반환한 값

- pid_t waitpid(pid_t pid, int *statloc , int options);

첫번째 인자

  • if (pid < -1) 프로세스 그룹 ID가 pid의 절댓값과 같은 자식 프로세스를 기다림
  • else if (pid == -1) 임의의 자식 프로세스를 기다림
  • else if (pid == 0) waitpid를 호출한 프로세스의 프로세스 그룹 PID와 같은 프로세스 그룹 ID를 가진 프로세스를 기다림
  • else if (pid > 0) 프로세스 ID가 pid인 자식 프로세스를 기다림

두번째 인자

  • if (자식 프로세스가 정상적으로 종료)
    waitpid 반환값 = 프로세스 id
    WIFEXITED(statloc) 매크로가 true를 반환
    하위 8비트를 참조하여 자식 프로세스가 exit, _exit, _Exit에 넘겨준 인자값을 얻을 수 있음, WEXITSTATUS(statloc)
  • else if(자식 프로세스가 비정상적으로 종료)
    waitpid 반환값 = 프로세스 id
    WIFSIGNALED(statloc) 매크로가 true를 반환
    비정상 종료 이유를 WTERMSIG(statloc) 매크로를 사용하여 구할 수 있음
  • else if(waitpid 함수 오류)
    waitpid 반환값 = -1
    ECHILD : 호출자의 자식 프로세스가 없는 경우
    EINTR : 시스템 콜이 인터럽트 되었을 때

세번째 인자

PID 란 ??

pid는 process id의 줄임말로 운영체제에서 프로세스를 식별하기 위해 부여하는 번호를 의미한다.
프로세스는 실행중인 프로그램을 의미한다. pid는 최근 할당한 pid에 1을 더한 값으로 할당해준다.

스레드란 ??

프로세스 내에서 실제로 작업을 수행하는 주체를 의미한다.(프로세스 내부 cpu 수행 단위) 모든 프로세스에는 한개 이상의 스레드가 존재하여 작업을 수행하고, 두 개 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스라고 한다.

참조

스레드(Thread)란?

비트연산자

연산자연산자의 기능
&비트단위로 AND 연산을 한다.
^비트단위로 XOR 연산을 한다.
~단항 연산자로서 피연자의 모든 비트를 반전시킨다.
<<피연산자의 비트 열을 왼쪽으로 이동시킨다.
>>피연산자의 비트 열을 오른쪽으로 이동시킨다.

각 연산자 예시

  • & 연산자 (둘다 1일때)
연산결과
0 & 00
0 & 10
1 & 00
1 & 11

#include<stdio.h>
int main()
{
	int num1 = 15;
    int num2 = 20;
    int num3 = num1 & num2;
    printf("and 연산 결과 : %d\n",num3);
}
///////////////과정//////////////////
/*
	00001111 => 15
 	&00010100 => 20
  	---------------
  	00000100 => 4
*/
///////////////결과//////////////////
/*
	and 연산 결과 : 4
*/
  • | 연산자 (하나라도 1이면)
연산결과
00
01
10
11
#include<stdio.h>
int main()
{
	int num1 = 15;
    int num2 = 20;
    int num3 = num1 | num2;
    printf("or 연산 결과 : %d\n",num3);
}
///////////////과정//////////////////
/*
	00001111 => 15
    &00010100 => 20
    ---------------
    00011111 => 31
*/
///////////////결과//////////////////
/*
	or 연산 결과 : 31
*/
  • 연산자 (서로 다를때 1)
연산결과
0 ^ 00
0 ^ 11
1 ^ 01
1 ^ 10
#include<stdio.h>
int main()
{
	int num1 = 15;
    int num2 = 20;
    int num3 = num1 ^ num2;
    printf("xor 연산 결과 : %d\n",num3);
}
///////////////과정//////////////////
/*
	00001111 => 15
    &00010100 => 20
    ---------------
    00011011 => 27
*/
///////////////결과//////////////////
/*
	xor 연산 결과 : 27
*/
  • 연산자 (반전, 보수연산)
연산결과
~ 01
~ 10
#include<stdio.h>
int main()
{
	int num1 = 15;
    int num2 = ~num1;
    printf("NOT 연산 결과 : %d\n",num2);
}
///////////////과정//////////////////
/*
	~00001111 => 15
    ---------
    00010000 => -16
*/
///////////////결과//////////////////
/*
	NOT 연산 결과 : -16
*/
  • 보수 예시
10진법2의 보수로 변환
70111
-81000
-nn-1을 뒤집은 결과
  • << 연산자 && >> 연산자

비트를 연산자 방향으로 이동시키는 shift연산이다.

#include<stdio.h>
int main()
{
	int num1 = 15;
    int num2 = num1 << 1;
    int num3 = num1 << 2;
    int num4 = num1 >> 1;
    int num5 = num1 >> 2;
    printf("<< 1칸이동 결과 : %d\n<< 2칸이동 결과 : %d\n",num2,num3);
    printf(">> 1칸이동 결과 : %d\n<< 2칸이동 결과 : %d\n",num4,num5);
}
///////////////과정//////////////////
/*
	00001111 
    00011110 << 1
    00111100 << 2
    00000111 >> 1
    00000011 >> 2
*/
///////////////결과//////////////////
/*
	<< 1칸이동 결과 : 30
    << 2칸이동 결과 : 60 // << 는 두배씩 적용되는것을 확인 할 수 있다.
    >> 1칸이동 결과 : 7
    << 2칸이동 결과 : 3
*/

리다이렉션

리눅스에서 프로그램은 보통 세개의 파일 서술자를 열게된다. 표준입력, 표준출력, 표준에러

  • cmd > file_name
    파일에 덮어쓰기 저장이 된다.
    ls -l 10 > test.txt 와 같이 작성을 하면 에러 발생으로 아무것도 저장되지 않는다.
    하지만 ls -l 10 2> test.txt 와 같이 작성하면 에러 메세지가 저장된다. 표준 입출력에러 012
  • cmd >> file_name
    파일에 이어서 저장이 된다. open함수를 사용할때 O_APPEND 옵션을 주면된다.
  • cmd < file_name
    file_name 의 파일 내용을 표준 입력으로 사용 한다는 의미이다.

cat test.txt
test text file!!
sed 's/test/@@@/' < test.txt
test @@@ file!!

< input cat | grep 'hi' > output
< input == input파일의 내용을 입력으로 사용한다.
cat | grep 'hi' > output == 입력에 대해 cat | grep 'hi' 한 출력을 output에 덮어쓰기한다.

here_doc

tag 혹은 eof에 도달할때까지의 입력을 표준입력으로 받는 방법이다.
cat << END 와 같이 사용하면
입력으로 END 혹은 eof를 주기전까지 입력을 받는다. 이후 커맨드를 실행 시킨다.
임시 tmp파일을 만들고 표준입력으로 get_next_line을 받아서 tag또는 eof일 떄 까지 파일에 써준다. 나중에 프로세스를 종료하기전에 unlink함수를 사용하여 지워주면 된다.

주의해야 할 점

1. 잘못되거나 없는 infile 혹은 command 일때

해당 에러를 출력해주어야 한다. 이때 입력이 없는 것으로 판단후 출력값은 전달해 커맨드들을 수행하거나 결과값을 전달해주어야한다.

예시

< infile ls | wc > outfile ==>infile은 없지만 결과는 oufile로 출력된다.
< infile cmd1 | wc > outfile ==> infile이 존재하고 cmd1이 잘못된 경우에도 비슷한 결과가 나온다.

2. infile 파일접근 관련

infile 의 chmod에 따른 접근을 체크해주면서 실행되어야한다.

3. 프로세스 병렬처리 (추후 minishell을 할때 필수이다)

fork를 통해 나눈 프로세스들이 병렬로 실행되어야한다.

예시

sleep 5 | sleep 3 이면 총 5초만 sleep되어야한다.

4. here_doc

tag값으로 입력을 끝낼때 tag와 정확히 일치하는지 체크해주어야 한다. eof또한 바로 종료

예시

cat << hi
11
22
hhi ---> 종료되면 안됨
hii ---> 종료되면 안됨
hi ---> 종료

5. 보너스 구현시

heredoc 일때와 아닐때의 input파일디스크립터를 설정해주고 dup2로 디스크립터 경로를 변경해주어야한다. 결과값도 마찬가지이고 파이프에서 파이프로 넘기는 과정은 while문 안에서 fork하고 자식프로세스에서 결과를 넘겨받는경우 직렬로 구현이 된다는점을 알아두어야한다. 병렬로 구현을 할시에는 fork 를 한번에 여러번 하면 된다.

예시

cat << hi
11
22
hhi ---> 종료되면 안됨
hii ---> 종료되면 안됨
hi ---> 종료

Mandatory 간단 구현

static void	child_work(t_arg *arg)
{
	arg->infile = open(arg->argv[1], O_RDONLY);
	if (arg->infile == -1)
		exit_err(arg, arg->argv[1], NULL, 1);
	// 올바른 커맨드인지 체크
	close(arg->pipe_fd[0]);//파이프는 항상 잘 닫아주어야함
	//dup2를 사용하여 입출력을 원하는 파일 디스크립터 혹은 파이프로 변경
	close(arg->pipe_fd[1]);
	close(arg->infile);
	if (execve(arg->cmd1, arg->cmd_arg1, arg->envp) == -1)// 실행
		exit_err(arg, "child execve error", NULL, 1);
}

static void	parent_work(t_arg *arg)
{
	waitpid(arg->pid, NULL, WNOHANG);//waitpid 를 사용하면 직렬로 된다. 자식프로세스가 종료될때 까지 기다리기 때문
	arg->outfile = open(arg->argv[4], O_RDWR | O_CREAT | O_TRUNC, 0644);// 없으면 생성
	if (arg->outfile == -1)
		exit_err(arg, arg->argv[4], NULL, 1);
	// 커맨드 체크
	close(arg->pipe_fd[1]);
	//dup2를 사용하여 입출력을 원하는 파일 디스크립터 혹은 파이프로 변경
	close(arg->pipe_fd[0]);
	close(arg->outfile);
	if (execve(arg->cmd2, arg->cmd_arg2, arg->envp) == -1)// 해당 명령어 실행하며 종료
		exit_err(arg, "parent execve error\n", NULL, 1);
}

int	main(int argc, char **argv, char **envp)
{
	t_arg	arg;

	init_arg(&arg);
	if (argc != 5)
		exit_err(&arg, NULL, "Wrong Usage\n", 1);
	parse_to_arg(&arg);
	if (pipe(arg.pipe_fd) < 0)
		exit_err(&arg, "pipe error", NULL, 1);
	arg.pid = fork();
	if (arg.pid < 0)
		exit_err(&arg, "fork error", NULL, 1);
	if (arg.pid == 0)
		child_work(&arg);
	else
		parent_work(&arg);
}

mandatory의 핵심은 입력받은 값을 자식 프로세스에서 execve함수를 통해 실행하고, 결과값을 부모 프로세스에 전달해준 다음 부모프로세스에서 실행을 하는 것 이다.

BONUS 간단 구현

mandatory와 다른점은 heredoc일때와 파일 덮어쓰기 그리고 멀티 파이프 정도이다. 만약 파이프를 구현할때 병렬처리되도록 구현하려면 그 갯수만큼 파이프를 따로 할당을 해주어야한다. 하지만 직렬로 할 떄에는 파이프 하나만으로도 가능은 하다.

static void	execute_cmd(t_arg *arg, int idx)
{
	if (!arg->cmd)
		exit_err(arg, "init err", 1);
	if (!arg->cmd[idx])
		exit_err(arg, "command not found", 127);
	execve(arg->cmd[idx], arg->cmd_arg[idx], arg->envp);
	exit_err(arg, "execve error", 1);
}

static void	pipe_work(t_arg *arg, int idx)
{
	if (pipe(arg->pipe) < 0)
		exit_err(arg, "pipe error", 1);
	arg->pid = fork();
	if (arg->pid < 0)
		exit_err(arg, "fork error", 1);
	if (arg->pid == 0)
		child_work(arg, idx);
	else
		parent_work(arg, idx);
}
 
int	main(int argc, char **argv, char **envp)
{
	t_arg	arg;
	int		idx;

	idx = -1;
	init_arg(&arg, argc, argv, envp);
	if (argc < 5)
		exit_err(&arg, "Wrong Usage", 1);
	parse_to_arg(&arg);
	set_infile_fd(&arg);
	while (++idx < arg.cmd_cnt - 1)
		pipe_work(&arg, idx);
	set_outfile_fd(&arg);
	execute_cmd(&arg, idx);
}

커맨드 -1 번만큼 while문을 돌며 자식 프로세스에서 cmd를 실행하고 부모에 결과값을 전달하며, 부모에서는 fork를 통해 프로세스를 나누어주며 나누어진 자식프로세스에서 결과값을 받아온다. 이후 마지막 cmd는 부모 프로세스에서 직접 실행시켜준다.

0개의 댓글