กระบวนการทดแทนถูกนำมาใช้ในการทุบตี?


12

ฉันกำลังค้นคว้าคำถามอื่นเมื่อฉันรู้ว่าฉันไม่เข้าใจว่าเกิดอะไรขึ้นภายใต้ประทุน/dev/fd/*ไฟล์เหล่านั้นคืออะไรและกระบวนการของเด็กสามารถเปิดได้อย่างไร


คำถามนั้นไม่ได้รับคำตอบใช่หรือไม่
phk

คำตอบ:


21

มันมีหลายด้านด้วยกัน

อธิบายไฟล์

สำหรับแต่ละกระบวนการเคอร์เนลจะรักษาตารางของไฟล์ที่เปิดอยู่ (เช่นกันมันอาจมีการใช้งานที่แตกต่างกัน แต่เนื่องจากคุณไม่สามารถมองเห็นมันได้เลยคุณสามารถสันนิษฐานได้ว่ามันเป็นตารางธรรมดา) ตารางนั้นมีข้อมูลเกี่ยวกับไฟล์ที่สามารถพบได้ในโหมดที่คุณเปิดซึ่งตำแหน่งที่คุณกำลังอ่าน / เขียนอยู่และตำแหน่งใดที่จำเป็นสำหรับการดำเนินการ I / O บนไฟล์นั้น ตอนนี้กระบวนการไม่เคยอ่าน (หรือแม้แต่เขียน) ตารางนั้น เมื่อกระบวนการเปิดไฟล์มันจะได้รับไฟล์ descriptor ที่เรียกกลับมา ซึ่งเป็นเพียงดัชนีในตาราง

ไดเรกทอรี/dev/fdและเนื้อหา

บน Linux dev/fdเป็นลิงก์สัญลักษณ์ไป/proc/self/fdแล้ว /procเป็นระบบไฟล์หลอกที่เคอร์เนลแมปโครงสร้างข้อมูลภายในหลายอย่างที่จะเข้าถึงด้วยไฟล์ API (ดังนั้นพวกเขาก็ดูเหมือนไฟล์ / ไดเรกทอรี / symlinks ปกติไปยังโปรแกรม) โดยเฉพาะอย่างยิ่งมีข้อมูลเกี่ยวกับกระบวนการทั้งหมด (ซึ่งเป็นชื่อที่ตั้งไว้) ลิงก์สัญลักษณ์/proc/selfหมายถึงไดเรกทอรีที่เกี่ยวข้องกับกระบวนการที่กำลังทำงานอยู่เสมอ (นั่นคือกระบวนการที่ร้องขอมันกระบวนการที่ต่างกันจึงจะเห็นค่าต่างกัน) ในไดเรกทอรีของกระบวนการมีไดเรกทอรีย่อยfd ซึ่งสำหรับไฟล์ที่เปิดแต่ละไฟล์จะมีลิงค์สัญลักษณ์ที่มีชื่อเป็นเพียงการแสดงทศนิยมของตัวอธิบายไฟล์ (ดัชนีลงในตารางไฟล์ของกระบวนการดูที่ส่วนก่อนหน้า) และเป้าหมายคือไฟล์ที่สอดคล้องกัน

ตัวอธิบายไฟล์เมื่อสร้างกระบวนการลูก

forkกระบวนการเด็กถูกสร้างขึ้นโดย A forkสร้างสำเนาของ file descriptors ซึ่งหมายความว่ากระบวนการลูกที่สร้างขึ้นมีรายการเปิดไฟล์เหมือนกับกระบวนการหลัก ดังนั้นหากหนึ่งในไฟล์ที่เปิดอยู่ถูกปิดโดยเด็กการเข้าถึง file descriptor ที่สืบทอดมาใน child จะเข้าถึงไฟล์เดียวกันกับการเข้าถึง file descriptor ดั้งเดิมในกระบวนการพาเรนต์

โปรดทราบว่าหลังจากที่แยกคุณเริ่มต้นมีกระบวนการเดียวกันสองชุดซึ่งแตกต่างกันเฉพาะในค่าส่งคืนจากการเรียก fork (ผู้ปกครองรับ PID ของเด็กลูกได้รับ 0) โดยปกติส้อมจะตามด้วยexecเพื่อแทนที่หนึ่งในสำเนาด้วยไฟล์ปฏิบัติการอื่น ตัวอธิบายไฟล์ที่เปิดอยู่รอดได้ที่ exec โปรดทราบด้วยว่าก่อนการประมวลผลกระบวนการสามารถดำเนินการอื่น ๆ (เช่นการปิดไฟล์ที่กระบวนการใหม่ไม่ควรได้รับหรือเปิดไฟล์อื่น ๆ )

ท่อที่ไม่มีชื่อ

ไพพ์ที่ไม่มีชื่อเป็นเพียงไฟล์ descriptors คู่หนึ่งที่สร้างขึ้นตามการร้องขอโดยเคอร์เนลดังนั้นทุกอย่างที่เขียนไปยังไฟล์ descriptor แรกจะถูกส่งไปยังวินาที การใช้งานทั่วไปส่วนใหญ่ใช้สำหรับการสร้าง piping foo | barของbashที่ซึ่งเอาต์พุตมาตรฐานของfooถูกแทนที่ด้วยส่วนการเขียนของไพพ์และอินพุตมาตรฐานจะถูกแทนที่ด้วยส่วนการอ่าน อินพุตมาตรฐานและเอาต์พุตมาตรฐานเป็นเพียงสองรายการแรกในตารางไฟล์ (รายการ 0 และ 1; 2 เป็นข้อผิดพลาดมาตรฐาน) ดังนั้นการแทนที่หมายถึงเพียงเขียนรายการตารางนั้นใหม่ด้วยข้อมูลที่สอดคล้องกับไฟล์อธิบายอื่น (อีกครั้ง การใช้งานจริงอาจแตกต่างกัน) เนื่องจากกระบวนการไม่สามารถเข้าถึงตารางได้โดยตรงมีฟังก์ชันเคอร์เนลที่ทำเช่นนั้น

การทดแทนกระบวนการ

ตอนนี้เรามีทุกอย่างเข้าด้วยกันเพื่อทำความเข้าใจว่าการทดแทนกระบวนการทำงานอย่างไร:

  1. กระบวนการทุบตีสร้างไปป์ที่ไม่มีชื่อสำหรับการสื่อสารระหว่างสองกระบวนการที่สร้างขึ้นในภายหลัง
  2. Bash forks สำหรับechoกระบวนการ กระบวนการลูก (ซึ่งเป็นสำเนาที่แน่นอนของbashกระบวนการต้นฉบับ) ปิดส่วนท้ายการอ่านของไพพ์และแทนที่เอาต์พุตมาตรฐานของตัวเองด้วยปลายเขียนของไพพ์ ระบุว่าechoเป็น shell builtin, bashอาจสำรองการexecโทร แต่ก็ไม่เป็นไร (shell builtin อาจถูกปิดการใช้งานด้วยเช่นกันในกรณีนี้ exec /bin/echo)
  3. Bash (ต้นฉบับหนึ่งพาเรนต์หนึ่ง) แทนที่นิพจน์<(echo 1)ด้วยลิงก์ไฟล์หลอกโดย/dev/fdอ้างอิงถึงจุดสิ้นสุดการอ่านของไปป์ที่ไม่มีชื่อ
  4. Bash execs สำหรับกระบวนการ PHP (โปรดทราบว่าหลังจากทางแยกเรายังคงอยู่ใน [สำเนาของ] ทุบตี) กระบวนการใหม่จะปิดปลายการเขียนที่สืบทอดมาของไปป์ที่ไม่มีชื่อ (และทำตามขั้นตอนการเตรียมการอื่น ๆ ) แต่เปิดทิ้งปลายการอ่านไว้ จากนั้นก็ดำเนินการ PHP
  5. โปรแกรม PHP ได้รับชื่อ/dev/fd/มา เนื่องจากตัวบ่งชี้ไฟล์ที่เกี่ยวข้องยังคงเปิดอยู่จึงยังคงสอดคล้องกับจุดสิ้นสุดการอ่านของไพพ์ ดังนั้นหากโปรแกรม PHP เปิดไฟล์ที่กำหนดไว้สำหรับการอ่านสิ่งที่มันทำคือการสร้างsecondไฟล์ descriptor สำหรับปลายอ่านของไพพ์ที่ไม่มีชื่อ แต่นั่นก็ไม่มีปัญหามันสามารถอ่านได้จากทั้งสอง
  6. ตอนนี้โปรแกรม PHP สามารถอ่านจุดสิ้นสุดการอ่านของไพพ์ผ่านตัวอธิบายไฟล์ใหม่และทำให้ได้รับเอาต์พุตมาตรฐานของechoคำสั่งซึ่งไปยังจุดสิ้นสุดการเขียนของไพพ์เดียวกัน

แน่นอนว่าฉันซาบซึ้งในความพยายามของคุณ แต่ฉันต้องการที่จะชี้ให้เห็นปัญหาต่าง ๆ ครั้งแรกที่คุณกำลังพูดคุยเกี่ยวกับphpสถานการณ์ แต่ไม่ได้จับท่ออย่างดีphp นอกจากนี้เมื่อพิจารณาคำสั่งcat <(echo test)ที่สิ่งที่แปลกที่นี่เป็นที่bashส้อมครั้งสำหรับแต่สองครั้งสำหรับcat echo test
x-yuri

13

การกู้ยืมเงินจากceltschkคำตอบ 's, การเชื่อมโยงสัญลักษณ์/dev/fd /proc/self/fdและ/procเป็นระบบแฟ้มหลอกที่นำเสนอข้อมูลเกี่ยวกับกระบวนการและข้อมูลระบบอื่น ๆ ในโครงสร้างคล้ายไฟล์แบบลำดับชั้น ไฟล์ที่/dev/fdสอดคล้องกับไฟล์ที่เปิดโดยกระบวนการและมี file descriptor เป็นชื่อและไฟล์เป็นเป้าหมาย การเปิดไฟล์/dev/fd/Nเทียบเท่ากับ descriptor ที่ซ้ำกันN(สมมติว่า descriptor 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ได้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.