Làm thế nào thay thế quá trình được thực hiện trong bash?


12

Tôi đang nghiên cứu một câu hỏi khác , khi tôi nhận ra mình không hiểu chuyện gì đang xảy ra, đó là những /dev/fd/*tập tin nào và làm thế nào để các tiến trình con có thể mở chúng.


Có phải câu hỏi đó đã được trả lời?
phk

Câu trả lời:


21

Vâng, có nhiều khía cạnh của nó.

Mô tả tập tin

Đối với mỗi quy trình, kernel duy trì một bảng các tệp đang mở (tốt, nó có thể được triển khai khác nhau, nhưng vì bạn không thể nhìn thấy nó bằng mọi cách, bạn chỉ có thể giả sử đó là một bảng đơn giản). Bảng đó chứa thông tin về tệp nào / nơi có thể tìm thấy, ở chế độ bạn đã mở, ở vị trí bạn đang đọc / ghi và ở bất kỳ vị trí nào khác cần thiết để thực hiện các thao tác I / O trên tệp đó. Bây giờ quá trình không bao giờ được đọc (hoặc thậm chí viết) bảng đó. Khi quá trình mở một tệp, nó sẽ lấy lại một cái gọi là mô tả tệp. Mà chỉ đơn giản là một chỉ mục vào bảng.

Thư mục /dev/fdvà nội dung của nó

Trên Linux dev/fdthực sự là một liên kết tượng trưng đến /proc/self/fd. /proclà một hệ thống tệp giả trong đó kernel ánh xạ một số cấu trúc dữ liệu nội bộ được truy cập bằng API tệp (vì vậy chúng trông giống như các tệp / thư mục / liên kết thông thường cho các chương trình). Đặc biệt là có thông tin về tất cả các quy trình (đó là cái đã đặt tên cho nó). Liên kết tượng trưng /proc/selfluôn đề cập đến thư mục được liên kết với quy trình hiện đang chạy (nghĩa là quá trình yêu cầu nó; do đó các quy trình khác nhau sẽ thấy các giá trị khác nhau). Trong thư mục của tiến trình, có một thư mục confd mà đối với mỗi tệp đang mở chứa một liên kết tượng trưng có tên chỉ là biểu diễn thập phân của bộ mô tả tệp (chỉ mục vào bảng tệp của quy trình, xem phần trước) và mục tiêu của nó là tệp tương ứng.

Mô tả tệp khi tạo các tiến trình con

Một quá trình con được tạo bởi a fork. A forktạo một bản sao của các mô tả tệp, có nghĩa là tiến trình con được tạo có cùng một danh sách các tệp đang mở giống như tiến trình cha mẹ. Vì vậy, trừ khi một trong những tệp đang mở được đóng bởi đứa trẻ, việc truy cập một bộ mô tả tệp được kế thừa ở trẻ sẽ truy cập vào cùng một tệp như truy cập vào bộ mô tả tệp gốc trong quy trình cha.

Lưu ý rằng sau một ngã ba, ban đầu bạn có hai bản sao của cùng một quy trình, chỉ khác nhau về giá trị trả về từ cuộc gọi ngã ba (cha mẹ nhận được PID của con, con được 0). Thông thường, một ngã ba được theo sau bởi một execđể thay thế một trong các bản sao bằng một thực thi khác. Các mô tả tập tin mở tồn tại mà thực hiện. Cũng lưu ý rằng trước khi thực thi, quy trình có thể thực hiện các thao tác khác (như đóng các tệp mà quy trình mới không nên nhận hoặc mở các tệp khác).

Ống không tên

Một ống không tên chỉ là một cặp mô tả tệp được tạo theo yêu cầu của kernel, để mọi thứ được ghi vào bộ mô tả tệp đầu tiên được chuyển sang thứ hai. Việc sử dụng phổ biến nhất là cho cấu trúc đường ống foo | barcủa bash, trong đó đầu ra tiêu chuẩn foođược thay thế bằng phần ghi của đường ống và đầu vào tiêu chuẩn được thay thế bằng phần đọc. Đầu vào tiêu chuẩn và đầu ra tiêu chuẩn chỉ là hai mục nhập đầu tiên trong bảng tệp (mục nhập 0 và 1; 2 là lỗi tiêu chuẩn), và do đó thay thế nó có nghĩa là chỉ viết lại mục nhập bảng đó với dữ liệu tương ứng với bộ mô tả tệp khác (một lần nữa, thực hiện thực tế có thể khác nhau). Vì quá trình không thể truy cập trực tiếp vào bảng, nên có một hàm kernel để làm điều đó.

Quá trình thay thế

Bây giờ chúng ta có mọi thứ cùng nhau để hiểu cách thức thay thế quá trình hoạt động:

  1. Quá trình bash tạo ra một đường ống không tên để liên lạc giữa hai quá trình được tạo sau này.
  2. Bash forks cho echoquá trình. Quá trình con (là bản sao chính xác của bashquy trình gốc ) đóng đầu đọc của ống và thay thế đầu ra tiêu chuẩn của chính nó bằng đầu viết của ống. Cho rằng đó echolà một phần mềm dựng sẵn, bashcó thể tự thực hiện execcuộc gọi, nhưng dù sao thì nó cũng không thành vấn đề (phần tích hợp trình bao này cũng có thể bị vô hiệu hóa, trong trường hợp nó thực thi /bin/echo).
  3. Bash (bản gốc, cha mẹ) thay thế biểu thức <(echo 1)bằng liên kết tệp giả trong /dev/fdtham chiếu đến đầu đọc của ống không tên.
  4. Bash thực thi quy trình PHP (lưu ý rằng sau khi rẽ nhánh, chúng ta vẫn ở trong [một bản sao của bash). Quá trình mới đóng kết thúc ghi được kế thừa của đường ống không tên (và thực hiện một số bước chuẩn bị khác), nhưng để kết thúc đọc mở. Sau đó, nó đã thực thi PHP.
  5. Chương trình PHP nhận được tên trong /dev/fd/. Vì bộ mô tả tệp tương ứng vẫn mở, nên nó vẫn tương ứng với đầu đọc của ống. Do đó, nếu chương trình PHP mở tệp đã cho để đọc, thì việc thực sự làm là tạo một bộ secondmô tả tệp cho đầu đọc của ống không tên. Nhưng đó không phải là vấn đề, nó có thể đọc từ một trong hai.
  6. Bây giờ chương trình PHP có thể đọc đầu đọc của ống thông qua bộ mô tả tệp mới và do đó nhận được đầu ra tiêu chuẩn của echolệnh đi đến đầu ghi của cùng một ống.

Chắc chắn, tôi đánh giá cao nỗ lực của bạn. Nhưng tôi muốn chỉ ra một số vấn đề. Đầu tiên, bạn đang nói về phpkịch bản, nhưng phpkhông xử lý tốt các đường ống . Ngoài ra, xem xét lệnh cat <(echo test), điều kỳ lạ ở đây là bashdĩa một lần cho cat, nhưng hai lần cho echo test.
x-yuri

13

Mượn từ celtschkcâu trả lời của, /dev/fdlà một liên kết tượng trưng đến /proc/self/fd. Và /proclà một hệ thống tệp giả, trình bày thông tin về các quy trình và thông tin hệ thống khác trong một cấu trúc giống như tệp phân cấp. Các tệp /dev/fdtương ứng với các tệp, được mở bởi một quy trình và có mô tả tệp là tên và tệp của chúng làm mục tiêu. Mở tệp /dev/fd/Ntương đương với sao chép mô tả N(giả sử rằng mô tả đó Nđang mở).

Và đây là kết quả điều tra của tôi về cách thức hoạt động của nó ( straceđầu ra loại bỏ các chi tiết không cần thiết và được sửa đổi để diễn đạt tốt hơn những gì đang xảy ra):

$ 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)

Về cơ bản, bashtạo một đường ống và chuyển các đầu của nó cho các phần tử con của nó dưới dạng mô tả tệp (đọc kết thúc 1.outvà viết kết thúc 2.out). Và chuyển kết thúc đọc dưới dạng tham số dòng lệnh tới 1.out( /dev/fd/63). Cách 1.outnày có thể mở /dev/fd/63.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.