Bash에서 프로세스 교체를 구현하는 방법은 무엇입니까?

Bash에서 프로세스 교체를 구현하는 방법은 무엇입니까?

연구 중이에요다른 문제, 뒤에서 무슨 일이 일어나고 있는지, /dev/fd/*이러한 파일이 무엇인지, 하위 프로세스가 파일을 어떻게 열었는지 이해하지 못했다는 것을 깨달았습니다.

답변1

글쎄요, 거기에는 많은 측면이 있습니다.

파일 설명자

각 프로세스에 대해 커널은 열린 파일 테이블을 유지 관리합니다(물론 다르게 구현될 수도 있지만 어쨌든 볼 수 없으므로 간단한 테이블이라고 가정할 수 있습니다). 이 테이블에는 파일이 있거나 찾을 수 있는 위치, 파일이 열리는 모드, 현재 읽고 쓰는 위치 및 실제로 파일에 대한 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파이프의 쓰기 부분으로 대체되고 표준 입력이 읽기 부분으로 대체되는 것입니다. stdin과 stdout은 파일 테이블의 처음 두 항목(항목 0과 1, 2는 표준 오류)일 뿐이므로 이를 바꾸는 것은 해당 테이블 항목을 다른 파일 설명자에 해당하는 데이터로 간단히 다시 쓰는 것을 의미합니다(실제 구현은 다를 수 있음). 프로세스가 테이블에 직접 액세스할 수 없기 때문에 이를 수행하는 커널 함수가 있습니다.

프로세스 대체

이제 모든 것이 준비되었으므로 프로세스 대체가 어떻게 작동하는지 확인할 수 있습니다.

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

답변2

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파이프를 생성하고 그 끝을 파일 설명자로 하위 항목에 전달합니다(끝을 읽고 1.out, 끝을 씁니다 2.out). 1.out( ) 에 명령줄 인수로 읽기 종료를 전달합니다 /dev/fd/63. 이 메소드는 1.out열 수 있습니다 /dev/fd/63.

관련 정보