뇌 마음 반반저장소

[42_pipex] 함수2. dup, dup2, execve, access, unlink 자세한 설명 본문

42/pipex

[42_pipex] 함수2. dup, dup2, execve, access, unlink 자세한 설명

맹진저 2023. 1. 7. 21:09
728x90

1.dup, dup2

이 함수는 파일 디스크립터를 복사하는 함수이다. 아마 duplicate이 어원일 것 같다..!

#include <unistd.h>
int	dup(int fd);
int	dup2(int fd, int fd2);
  • dup은 매개변수 fd를 복제하여 반환한다. 성공 시 새 fd, 오류시 -1을 반환한다.
  • dup2는 매개변수 fd를 fd2로 복제한다. 반일 fd2가 이미 열려있다면 자체적으로 close를 한 후 복제한다. 성공 시 새 fd, 오류 시 -1을 반환한다.
  • 메모리 공간만 복사된다. 

그럼 예시를 들어보자! 일단 복사를 하면 fd의 번호가 어떻게 정렬되는지 확인해 보자.

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

# define BUFFER_SIZE    1024

int main()
{
    int fd1[2];
    int fd2;
    char    tmp[BUFFER_SIZE];

    pipe(fd1); //파이프로 fd1의 통로를 열어준다.
    fd2 = dup(fd1[1]); //fd1에 같은 공간을 복사해준다.
    printf("fd1[1] : %d\nfd2 : %d\n", fd1[1], fd2);
    
    close(fd1[2]);
    close(fd2);
}
$ ./a.out
fd1[1] : 4 #원본 fd1의 out 위치
fd2 : 5 #복사된 파일의 순서

4, 5로 차례로 정렬이 잘 되었다. 아마도 fd[0]은 3에 배치가 되었을 것이다. 그러면 문자를 한번 넣어보자.

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

# define BUFFER_SIZE    1024

int main()
{
    int fd1[2];
    int fd2;
    char    tmp[BUFFER_SIZE];

    pipe(fd1);
    fd2 = dup(fd1[1]);
    printf("fd1[1] : %d\nfd2 : %d\n", fd1[1], fd2);
    
    fd1[0] = open("./text.txt", O_RDWR); //text 파일을 열어주고
    read(fd1[0], tmp, BUFFER_SIZE); //그 안에있는 텍스트를 tmp에 복사하고
    printf("fd1 text : %s\n", tmp); //출력해 본다.
    write(fd2, tmp, BUFFER_SIZE); //그리고 fd2파일에 써준다.
    printf("fd2 text : %s\n", tmp); //출력해 본다

    close(fd1[2]);
    close(fd2);
}
$ ./a.out
fd1[1] : 4 
fd2 : 5 
fd1 text : Hello
fd2 text : Hello

 

그렇다면 dup2은 어떨까?

int main()
{
    int fd1[2];
    int fd2[2];
    char    tmp[BUFFER_SIZE];

    pipe(fd1);
    pipe(fd2);
    fd2[2] = dup2(fd1[1], fd2[1]);
    printf("fd1[0] : %d\n", fd1[0]);
    printf("fd1[1] : %d\n", fd1[1]);
    printf("fd2[0] : %d\n", fd2[0]);
    printf("fd2[1] : %d\n", fd2[1]);

    close(fd1[2]);
    close(fd2[2]);
}
$ ./a.out
fd1[0] : 3
fd1[1] : 4
fd2[0] : 5
fd2[1] : 6

여기도 순차적으로 잘 확장이 되었다!

 

STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO은 변수로 사용가능할까?👇

응용 예제를 보기전에 아래의 포스팅에서 2번 카테고리를 보고 오는 것을 추천한다.

[42_pipex] 탐구1. pipex 구조 자세히 살펴보기 및 예제
2. 0, 1, 2... 표준 입력(stdin)과 표준 출력(stdout), 표준 에러(stderr) 예제

 

아래의 예제를 보자.

 

fd의 자동할당 구조

새로운 파일 디스크립터를 열면 이렇게 자동으로 할당이 된다.

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

# define    BUFFER_SIZE 1024

void    main()
{
    int fd;

    fd = open("./text.txt", O_RDWR);
    printf("name of file : STDIN_FILENO, num of stdin : %d\n", STDIN_FILENO);
    printf("name of file : STDOUT_FILENO, num of stdout : %d\n", STDOUT_FILENO);
    printf("name of file : STDERROR_FILENO, num of stdin : %d\n", STDERR_FILENO);
    printf("name of file : ./text.txt, num of fd : %d\n", fd);
    close(fd);
}
$ ./a.out
name of file : STDIN_FILENO, num of stdin : 0
name of file : STDOUT_FILENO, num of stdout : 1
name of file : STDERROR_FILENO, num of stdin : 2
name of file : ./text.txt, num of fd : 3

하지만 dup2를 사용해서 fd를 STDIN_FILENO, 즉 인풋 파일로 복사해 준다면?

void    main()
{
    int fd;
    char    tmp[BUFFER_SIZE];

    fd = open("./text.txt", O_RDWR);
    read(fd, tmp, BUFFER_SIZE);
    fd = dup2(fd, STDIN_FILENO);
    printf("name of file : STDIN_FILENO, num of stdin : %d\n", STDIN_FILENO);
    printf("name of file : STDOUT_FILENO, num of stdout : %d\n", STDOUT_FILENO);
    printf("name of file : STDERROR_FILENO, num of stdin : %d\n", STDERR_FILENO);
    printf("name of file : ./text.txt, num of fd : %d\n", fd);
    close(fd);
}
$ ./a.out
name of file : STDIN_FILENO, num of stdin : 0
name of file : STDOUT_FILENO, num of stdout : 1
name of file : STDERROR_FILENO, num of stdin : 2
name of file : ./text.txt, num of fd : 0

위의 결과를 보면 fd 가 0번으로 바뀌었다. 아래의 이미지처럼 설명할 수 있다.

dup2가 가리키는 방향

void    main()
{
    int fd;
    char    tmp[BUFFER_SIZE];

    fd = open("./text.txt", O_RDWR);
    fd = dup2(fd, STDIN_FILENO);
    printf("name of file : STDIN_FILENO, num of stdin : %d\n", STDIN_FILENO);
    printf("name of file : STDOUT_FILENO, num of stdout : %d\n", STDOUT_FILENO);
    printf("name of file : STDERROR_FILENO, num of stdin : %d\n", STDERR_FILENO);
    printf("name of file : ./text.txt, num of fd : %d\n\n", fd);
    read(STDIN_FILENO, tmp, BUFFER_SIZE);
    printf("read STDIN_FILENO : %s\n", tmp);
    close(fd);
}
$ ./a.out
name of file : STDIN_FILENO, num of stdin : 0
name of file : STDOUT_FILENO, num of stdout : 1
name of file : STDERROR_FILENO, num of stdin : 2
name of file : ./text.txt, num of fd : 0

read STDIN_FILENO : Hello

dup을 사용해서 STDIN_FILENO를 복사해 준다면?

void    main()
{
    int fd;

    fd = dup(STDIN_FILENO);
    printf("name of file : STDIN_FILENO, num of stdin : %d\n", STDIN_FILENO);
    printf("name of file : STDOUT_FILENO, num of stdout : %d\n", STDOUT_FILENO);
    printf("name of file : STDERROR_FILENO, num of stdin : %d\n", STDERR_FILENO);
    printf("num of fd : %d\n", fd);
    close(fd);
}
$ ./a.out
name of file : STDIN_FILENO, num of stdin : 0
name of file : STDOUT_FILENO, num of stdout : 1
name of file : STDERROR_FILENO, num of stdin : 2
num of fd : 3

결과를 보면 fd의 순서는 3번이지만 실제적으로는 fd(3)는 STDIN_FILENO, 즉 fd(0)을 가리키고 있다.

dup이 가리키는 방향


2. execve

exec 함수 패밀리들은 현재 프로세스는 종료하고 새로운 프로세스로 대체한다. exec의 어원은 execution이라는 실행, 집행, 사형집행(?)이라는 뜻을 가진 단어에서 왔다. 그리고 그 뒤에 하나이상의 문자가 붙어서 여러 함수의 종류가 탄생한다.

 

e : 환경 변수에 대한 포인터 배열이 새 프로세스 이미지에 명시적으로 전달됩니다.

l : 명령줄 인수가 함수에 개별적으로(목록) 전달됩니다.

p : PATH 환경 변수를 사용하여 실행할 파일로 인수에 이름이 지정된 파일을 찾습니다.

v : 명령줄 인수는 포인터의 배열(벡터)로 함수에 전달됩니다.

execl, execlp, execv 및 execvp를 호출하면 현재 환경 변수를 가지고 새 프로세스를 시작합니다. (내 것을 강조합니다.)

exec계열함수 중 execve(2)만 "system call 함수"이고, 나머지 execl(3), execlp(3), execle(3), execv(3), execvp(3), execvpe(3)는 execve(3)에 "wrapping을 한 함수" 입니다.

What does the "ve" in "execve" mean?

 

그렇다면 exec + ve 뜻은 : 현재 프로세스를 종료하고 새로운 프로세스로 대체하는데, 포인터 배열을 함수로 전달하고, 환경변수의 포인터 배열도 새 프로세스에 전달된다. 그래서 아래에 많은 배열들이 매개변수로 들어가는 것을 볼 수가 있다.

#include <unistd.h>
int	execve(const char *pathname, char *const argv[], char *const envp[]);
  • pathname은 path의 파일을 실행한다. argv[0]은 첫 번째 인자인 프로그램의 이름을 가져오기 때문에 파일의 정확한 경로를 모두 입력해 주는 것이 좋다.
  • argv는 인수의 목록을 가져온다. 배열의 마지막 값은 NULL이어야 한다.
  • envp는 환경 설정 목록을 가져온다. 
  • 반환값이 없다면 정상적으로 실행된 것이다. 왜냐하면 이미 프로그램에서 실행되어 나갔기 때문. 반환값 -1은 실패 이유와 함께 반환된다.

const char / char *const / char *const의 다른 의미 알아보기 👉 [42] const char의 *위치에 따른 다른 의미

 

execve 함수 돌아가네~

아래와 같이 함수를 실행시켜 보자.

#include <unistd.h>

void main(int argc, char *argv[])
{
    execve("/bin/ls", argv, NULL);
}

그리고 프로그램을 실행하면 결과는!

$ ./a.out
a.out  dup.c  execve.c  fork.c  pipe.c  text.txt  text2.txt

마치 내가 ls를 입력한 것처럼 프로그램이 돌아간다. 😃 

 

프로그램 뒤에 다른 명령어들을 입력한다면?

$ ./a.out -l
total 29
-rwxrwxrwx 1 myID myID 16696 Jan  4 21:48 a.out
-rwxrwxrwx 1 myID myID   653 Jan  4 18:40 dup.c
-rwxrwxrwx 1 myID myID    98 Jan  4 21:48 execve.c
-rwxrwxrwx 1 myID myID   669 Jan  4 20:14 fork.c
-rwxrwxrwx 1 myID myID   358 Jan  4 17:59 pipe.c
-rwxrwxrwx 1 myID myID  1043 Jan  4 18:40 text.txt
-r-xr-xr-x 1 myID myID     0 Jan  4 18:39 text2.txt

PATH

Pipex를 만들다가 "음? 그러면 환경변수가 정해지지 않았을 때는 어떻게 가져오지?"라는 의문을 갖게 되었다. 바로 환경변수를 PATH로 찾아서 넣어주어야 한다. 일단 언제나 그랬듯 기본부터 확인해 보자.

환경 변수(環境 變數, 영어: environment variable)는 프로세스가 컴퓨터에서 동작하는 방식에 영향을 미치는, 동적인 값들의 모임이다.

어느 블로거님이 쓰신 아주 통쾌한 설명을 인용해 본다.

일단 '환경변수'라는 이름만 놓고 보더라도 일종의 변수라는 것을 알수 있는데, (여기서는 프로그램상의 변수를 의미합니다) os에서 자식프로세스들을 생성할때 참조하는 '변수들' 이라고 생각하면 아주 좋다.

사실 우리가 컴퓨터로 뛰우는 모든 작업창(게임, 인터넷 익스플로러, 제어판, 그림판 등등....) 조금더 엄밀히 얘기해서 프로세스들은 전부 os라는 프로그램에 의해 실행되는 자식 프로세스들이다. 바로 이때 os입장에서 해당 프로세스를 실행시키기위해 참조하는 변수가 바로 이 환경변수가 되겟다.
이와 같은 환경변수중에서 일반인 유저를 포함해서 컴퓨터를 쓴다면 자주 마주칠법한 변수가 하나 있는데,
그녀석이 바로 그이름도 악명높은 path라는 이름의 변수 되시겟다. 이 path변수는 운영체제가 어떤 프로세스를 실행시킬때, 그 경로를 찾는데 이용된다. 명령프롬프트 상에서(윈도우 기준) 환경변수 설정은 SET을 통해 조작할수 있으며, 현재 등록된 모든 환경변수들의 이름과 값들을 확인할수 있다. 그리고 저 악명높은 path변수를 위해 그만을 위한 명령어를 따로 두고 있는데 (ㅎㄷㄷ...) 명령어명은 그냥 말그대로 path다. 이에 대한 자세한 설명은 후에 기회가 되면 올려놓도록 하겟다.

1-1 환경변수란? 환경변수와 path 에서 인용

그럼 환경 변수는 왜 사용할까? 

환경변수는 바로가기처럼 미리 지정해 놓은 루트로 쑝 간다. 그럼 아래의 예시를 보자. 아래는 환경변수를 정하지 않고 모두 쓴다음에 파일을 실행한다.

 

미리 sub-sub-directoy에 path_test.sh 라는 파일을 만들어 실행하면 Hello Path라는 문구가 나오게 만들었다.

$ cat pipex/test_ls/path_test.sh
echo Hello Path

path_test.sh를 실행시키면 이렇게 문장이 뜬다. 

$ sh pipex/test_ls/path_test.sh
Hello Path

위의 길고 긴(?) 경로를 미리 정해주면 저렇게 하나하나 쓸 필요가 없어진다. 환경변수를 만들어 보자.

$ path_test=pipex/test_ls/path_test.sh

간단하게 "경로의 별명=경로/.../파일"로 정해주면 바로 경로로 이동하여 파일을 실행한다.

실행할 때는 경로의 별명 앞에 달러표시 $를 붙여주면 된다.

$ $path_test
Hello Path

 

나의 환경변수를 확인하려면

$ echo $path_test
pipex/test_ls/path_test.sh

만든 환경변수를 삭제하려면 

$ unset path_test

 

나의 환경변수를 알아보려면 쉘에 env를 쳐보자.

$ env
SHELL=/bin/bash
WSL_DISTRO_NAME=Ubuntu-20.04
NAME=**********
PWD=//
LOGNAME=*****
HOME=*****
LANG=C.UTF-8
WSL_INTEROP=/run/WSL/161_interop
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;...
LESSCLOSE=/usr/bin/lesspipe %s %s
TERM=xterm-256color
LESSOPEN=| /usr/bin/lesspipe %s
USER=*****
SHLVL=1
WSLENV=
XDG_DATA_DIRS=/usr/local/share:/usr/share:/var/lib/snapd/desktop
PATH=/home/*****/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/...
HOSTTYPE=x86_64
OLDPWD=****************
_=/usr/bin/env

우리가 사용해야하는 PATH 환경변수가 드디어 나왔다.

PATH의 모든 디렉토리가 :로 구분된다. 그래서 정말 많은 폴더들이 나열된다.

 

위의 execve 함수에서 ls 명령어의 환경변수를 가져오기 위해 디렉토리 경로를 매개변수로 가져오고, 맨 마지막에는 그에 따른 환경변수를 가져왔다. 이렇듯 우리는 PATH경로에서 폴더를 찾아 그에 따른 환경변수를 넣어 줘야 한다...!

 

나머지는 코딩을 짜면서 알아보겠닷!


먼저 이 포스팅을 읽으면 아래 함수를 이해하는데 도움이 많이 된다!

[42_pipex] 탐구4. inode, 파일권한(chmod), 링크파일(하드, 심볼릭) 쉽게 알아보기

3.access

이 함수는 프로세스가 파일에 액세스 할 수 있는지 여부를 확인해 준다. 경로 이름이 심볼릭 링크라면 참조가 취소된다..! (Linux man- ACCESS(2))

#include <unistd.h>
int	access(const char *pathname, int mode);
  • 반환값은 mode가 ok이면 성공을 의미하며 0을, 실패하면 -1을 반환한다.
  • 첫 번째 매개변수는 파일의 이름을 의미한다.
  • 두 번째 매개변수는 여러 가지 모드가 있다.
  • R_OK : 읽기가 가능한가?
  • W_OK : 쓰기가 가능한가?
  • X_OK : 실행이 가능한가?
  • F_OK : 파일이 존재하는가?

예를 들어보자. c.txt파일을 테스트해보겠다. 아래의 inode 정보를 우선 확인해 보자.

$ ls -il | grep c.txt
32088147345025272 -rwxrwxrwx 1 myID myID    25 Jan  6 18:26 c.txt

파일의 권한이 활~짝 열린 chmod 777의 파일이다.

#include <unistd.h>
#include <stdio.h>

void    main()
{
    printf("Read possible? : %s\n", access("./c.txt", R_OK) == 0 ? "possible" : "impossible");
    printf("Write possible? : %s\n", access("./c.txt", W_OK) == 0 ? "possible" : "impossible");
    printf("Execute possible? : %s\n", access("./c.txt", X_OK) == 0 ? "possible" : "impossible");
    printf("File is exist? : %s\n", access("./c.txt", F_OK) == 0 ? "possible" : "impossible");
}

결과는?

$ ./a.out
Read possible? : possible
Write possible? : possible
Execute possible? : possible
File is exist? : possible

4.unlink

이 친구는 링크파일 또는 그냥 파일도! 삭제해주는 함수이다.

#include <unistd.h>
int	unlink(const char *pathname);
  • 파일 시스템에서 이름을 삭제해 준다.
  • 해당 이름 파일에 대한 미지막 링크이거나, 파일이 열려있는 프로세스가 없는 경우 파일이 삭제되고, 이 파일이 사용하던 공간을 재사용할 수 있다. 
  • 만약에 파일이 열려있는 프로세스가 아직 진행 중이라면, 파일을 마지막으로 참조하는 fd가 닫힐 때까지 삭제할 수 없다.
  • 만약에 심볼릭 링크 파일이라면 링크가 제거된다.
  • 소켓, 파이프 또는 장치파일이라면 가리키는 이름은 제거되지만 개체가 열려있는 프로세스는 계속 사용할 수 있다.

unlink를 사용해서 파일을 지워보자.

$ ls
A  B  C  a.c  a.out  access.c  b.h  c.txt

이렇게 있던 파일이,

#include <unistd.h>

void    main()
{
    unlink("./a.c");
    unlink("./b.h");
    unlink("./c.txt");
    unlink("./a.out");
    unlink("./access.c");
}
$ ./a.out
$ ls
A  B  C

사라졌다!

 

자 그러면 본격적으로 Pipex를 만들어볼까!

728x90
Comments