bash에서 프로세스 대체가 어떻게 구현됩니까?


12

나는 다른 질문 을 연구하고 있었는데 , 나는 후드에서 무슨 일이 일어나고 있는지, 그 /dev/fd/*파일 이 무엇인지, 자식 프로세스가 어떻게 그것을 열 수 있는지 이해하지 못한다는 것을 깨달았 습니다.


그 질문에 대답하지 않습니까?
phk

답변:


21

글쎄, 그것에 많은 측면이 있습니다.

파일 기술자

각 프로세스에 대해 커널은 열린 파일 테이블을 유지 관리합니다 (잘 다르게 구현 될 수 있지만 어쨌든 볼 수 없으므로 간단한 테이블이라고 가정 할 수 있습니다). 이 테이블에는 파일 위치, 파일 위치, 파일을 연 모드, 현재 읽고 쓰는 위치 및 해당 파일에 대한 I / O 작업을 실제로 수행하는 데 필요한 기타 정보가 포함되어 있습니다. 이제 프로세스는 해당 테이블을 읽거나 쓰지 않습니다. 프로세스가 파일을 열면 소위 파일 디스크립터를 다시 가져옵니다. 이것은 단순히 테이블에 대한 인덱스입니다.

디렉토리 /dev/fd와 그 내용

Linux dev/fd에서 실제로는에 대한 심볼릭 링크 /proc/self/fd입니다. /proc커널은 파일 API로 액세스 할 여러 내부 데이터 구조를 커널이 맵핑하는 의사 파일 시스템입니다 (따라서 일반 파일 / 디렉토리 / 프로그램에 대한 심볼릭 링크처럼 보입니다). 특히 모든 프로세스에 대한 정보가 있습니다 (이름을 부여한 것임). 기호 링크는 /proc/self항상 현재 실행중인 프로세스 (즉, 프로세스를 요청하는 프로세스와 연관되어 있으므로 다른 프로세스는 다른 값을 볼 수 있음)와 관련된 디렉토리를 나타냅니다. 프로세스의 디렉토리에는 하위 디렉토리가 있습니다fd 열려있는 각 파일에 대해 이름은 파일 디스크립터의 십진 표시 (프로세스 파일 테이블의 색인, 이전 섹션 참조)이고 대상이 해당 파일 인 파일의 기호 링크를 포함합니다.

자식 프로세스를 만들 때의 파일 디스크립터

자식 프로세스는에 의해 생성됩니다 fork. A fork는 파일 디스크립터의 사본을 작성하는데, 이는 작성된 하위 프로세스가 상위 프로세스와 동일한 열린 파일 목록을 가지고 있음을 의미합니다. 따라서 열린 파일 중 하나가 자식에 의해 닫히지 않는 한 자식의 상속 된 파일 설명자에 액세스하면 부모 프로세스의 원래 파일 설명자에 액세스하는 것과 동일한 파일에 액세스합니다.

포크 후에는 처음에 포크 호출과의 리턴 값 만 다른 동일한 프로세스의 두 사본이 있습니다 (부모는 자식의 PID를 가져오고 자식은 0을 얻습니다). 일반적으로 포크 다음에 exec는 사본 중 하나를 다른 실행 파일로 대체합니다. 열린 파일 디스크립터는 그 실행 후에도 유지됩니다. 또한 실행 전에는 프로세스가 다른 조작을 수행 할 수 있습니다 (예 : 새 프로세스가 가져 오지 않아야하는 파일 닫기 또는 다른 파일 열기).

명명되지 않은 파이프

명명되지 않은 파이프는 커널이 요청하면 생성 된 파일 디스크립터 쌍이므로 첫 번째 파일 디스크립터에 기록 된 모든 것이 두 번째 파일 디스크로 전달됩니다. 가장 일반적인 용도는의 파이프 구성 foo | bar에 사용되며 bash, 여기서 표준 출력은 foo파이프의 쓰기 부분으로 대체되고 표준 입력은 읽기 부분으로 대체됩니다. 표준 입력 및 표준 출력은 파일 테이블의 처음 두 항목 (항목 0 및 1; 2는 표준 오류 임)이므로 해당 테이블 항목을 다른 파일 디스크립터에 해당하는 데이터 (다시 말해서 실제 구현은 다를 수 있습니다). 프로세스가 테이블에 직접 액세스 할 수 없으므로이를 수행하는 커널 함수가 있습니다.

공정 대체

이제 우리는 프로세스 대체가 어떻게 작동하는지 이해하기 위해 모든 것을 갖추고 있습니다.

  1. bash 프로세스는 나중에 작성된 두 프로세스 간의 통신을 위해 이름없는 파이프를 작성합니다.
  2. echo프로세스를 위한 배쉬 포크 . 하위 프로세스 (원래 bash프로세스 의 정확한 사본 )는 파이프의 읽기 끝을 닫고 자체 표준 출력을 파이프의 쓰기 끝으로 바꿉니다. 그것이 echo쉘 내장 이라고 가정 bash하면 exec호출 자체를 아낄 수는 있지만 어쨌든 중요하지 않습니다 (쉘 내장 기능도 비활성화 될 수 있습니다 /bin/echo. 이 경우 execs ).
  3. Bash (원본의 상위 항목)는 명명되지 않은 파이프의 읽기 끝을 참조 <(echo 1)하여 의사 파일 링크로 표현식 을 대체합니다 /dev/fd.
  4. PHP 프로세스를위한 Bash execs (포크 후에도 여전히 bash의 사본 안에 있음). 새 프로세스는 이름이 지정되지 않은 파이프의 상속 된 쓰기 끝을 닫고 다른 준비 단계를 수행하지만 읽기 끝은 열어 둡니다. 그런 다음 PHP를 실행했습니다.
  5. PHP 프로그램은에 이름을받습니다 /dev/fd/. 해당 파일 디스크립터가 여전히 열려 있기 때문에 여전히 파이프의 읽기 끝에 해당합니다. 따라서 PHP 프로그램이 주어진 파일을 읽기 위해 열면 실제로 수행하는 것은 second명명되지 않은 파이프의 읽기 끝을위한 파일 디스크립터 를 작성하는 것 입니다. 그러나 그것은 문제가되지 않습니다. 어느 쪽이든 읽을 수 있습니다.
  6. 이제 PHP 프로그램은 새 파일 디스크립터를 통해 파이프의 읽기 끝을 읽을 수 있으므로 echo동일한 파이프의 쓰기 끝으로 이동 하는 명령 의 표준 출력을 수신합니다 .

당신의 노력에 감사드립니다. 그러나 몇 가지 문제를 지적하고 싶었습니다. 먼저 php시나리오에 대해 이야기하고 있지만 php파이프를 잘 처리하지 못합니다 . 또한 명령을 고려할 cat <(echo test)때 이상한 점은 bash포크는 한 번 cat, 두 번은 포크 입니다 echo test.
x-yuri April

13

celtschk대답 에서 빌려온 /dev/fd것은에 대한 상징적 인 링크 /proc/self/fd입니다. 또한 /proc의사 파일 시스템으로, 프로세스 및 기타 시스템 정보에 대한 정보를 계층 적 파일 구조로 제공합니다. 파일은 파일에 /dev/fd해당하며 프로세스에 의해 열리고 파일 디스크립터는 이름으로, 파일 자체는 대상으로합니다. 파일을 여는 것은 /dev/fd/N설명자를 복제 N한다고 가정합니다 (설명자가 N열려 있다고 가정 ).

그리고 그것이 어떻게 작동하는지에 대한 나의 조사 결과는 다음과 같습니다 ( strace출력은 불필요한 세부 사항을 제거하고 현재 상황을 더 잘 표현하도록 수정되었습니다).

$ cat 1.c
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    char buf[100];
    int fd;
    fd = open(argv[1], O_RDONLY);
    read(fd, buf, 100);
    write(STDOUT_FILENO, buf, n_read);
    return 0;
}
$ gcc 1.c -o 1.out
$ cat 2.c
#include <unistd.h>
#include <string.h>

int main(void)
{
    char *p = "hello, world\n";
    write(STDOUT_FILENO, p, strlen(p));
    return 0;
}
$ gcc 2.c -o 2.out
$ strace -f -e pipe,fcntl,dup2,close,clone,close,execve,wait4,read,open,write bash -c './1.out <(./2.out)'
[bash] pipe([3, 4]) = 0
[bash] dup2(3, 63) = 63
[bash] close(3) = 0
[bash] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p2
Process p2 attached
[bash] close(4) = 0
[bash] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p1
Process p1 attached
[bash] close(63) = 0
[p2] dup2(4, 1) = 1
[p2] close(4) = 0
[p2] close(63) = 0
[bash] wait4(-1, <unfinished ...>
Process bash suspended
[p1] execve("/home/yuri/_/1.out", ["/home/yuri/_/1.out", "/dev/fd/63"], [/* 31 vars */]) = 0
[p2] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p22
Process p22 attached
[p22] execve("/home/yuri/_/2.out", ["/home/yuri/_/2.out"], [/* 31 vars */]) = 0
[p2] wait4(-1, <unfinished ...>
Process p2 suspended
[p1] open("/dev/fd/63", O_RDONLY) = 3
[p1] read(3,  <unfinished ...>
[p22] write(1, "hello, world\n", 13) = 13
[p1] <... read resumed> "hello, world\n", 100) = 13
Process p2 resumed
Process p22 detached
[p1] write(1, "hello, world\n", 13) = 13
hello, world
[p2] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = p22
[p2] --- SIGCHLD (Child exited) @ 0 (0) ---
[p2] wait4(-1, 0x7fff190f289c, WNOHANG, NULL) = -1 ECHILD (No child processes)
Process bash resumed
Process p1 detached
[bash] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = p1
[bash] --- SIGCHLD (Child exited) @ 0 (0) ---
Process p2 detached
[bash] wait4(-1, 0x7fff190f2bdc, WNOHANG, NULL) = 0
--- SIGCHLD (Child exited) @ 0 (0) ---
[bash] wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG, NULL) = p2
[bash] wait4(-1, 0x7fff190f299c, WNOHANG, NULL) = -1 ECHILD (No child processes)

기본적으로 bash파이프를 작성하고 그 끝을 파일 설명 자로 하위에 전달합니다 (end를 읽고, end를 1.out쓰는 것 2.out). 그리고 명령 행 매개 변수로 read end를 1.out( /dev/fd/63)에 전달합니다. 이 방법 1.out은 열 수 있습니다 /dev/fd/63.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.