ฉันกำลังค้นคว้าคำถามอื่นเมื่อฉันรู้ว่าฉันไม่เข้าใจว่าเกิดอะไรขึ้นภายใต้ประทุน/dev/fd/*
ไฟล์เหล่านั้นคืออะไรและกระบวนการของเด็กสามารถเปิดได้อย่างไร
ฉันกำลังค้นคว้าคำถามอื่นเมื่อฉันรู้ว่าฉันไม่เข้าใจว่าเกิดอะไรขึ้นภายใต้ประทุน/dev/fd/*
ไฟล์เหล่านั้นคืออะไรและกระบวนการของเด็กสามารถเปิดได้อย่างไร
คำตอบ:
มันมีหลายด้านด้วยกัน
อธิบายไฟล์
สำหรับแต่ละกระบวนการเคอร์เนลจะรักษาตารางของไฟล์ที่เปิดอยู่ (เช่นกันมันอาจมีการใช้งานที่แตกต่างกัน แต่เนื่องจากคุณไม่สามารถมองเห็นมันได้เลยคุณสามารถสันนิษฐานได้ว่ามันเป็นตารางธรรมดา) ตารางนั้นมีข้อมูลเกี่ยวกับไฟล์ที่สามารถพบได้ในโหมดที่คุณเปิดซึ่งตำแหน่งที่คุณกำลังอ่าน / เขียนอยู่และตำแหน่งใดที่จำเป็นสำหรับการดำเนินการ 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 เป็นข้อผิดพลาดมาตรฐาน) ดังนั้นการแทนที่หมายถึงเพียงเขียนรายการตารางนั้นใหม่ด้วยข้อมูลที่สอดคล้องกับไฟล์อธิบายอื่น (อีกครั้ง การใช้งานจริงอาจแตกต่างกัน) เนื่องจากกระบวนการไม่สามารถเข้าถึงตารางได้โดยตรงมีฟังก์ชันเคอร์เนลที่ทำเช่นนั้น
การทดแทนกระบวนการ
ตอนนี้เรามีทุกอย่างเข้าด้วยกันเพื่อทำความเข้าใจว่าการทดแทนกระบวนการทำงานอย่างไร:
echo
กระบวนการ กระบวนการลูก (ซึ่งเป็นสำเนาที่แน่นอนของbash
กระบวนการต้นฉบับ) ปิดส่วนท้ายการอ่านของไพพ์และแทนที่เอาต์พุตมาตรฐานของตัวเองด้วยปลายเขียนของไพพ์ ระบุว่าecho
เป็น shell builtin, bash
อาจสำรองการexec
โทร แต่ก็ไม่เป็นไร (shell builtin อาจถูกปิดการใช้งานด้วยเช่นกันในกรณีนี้ exec /bin/echo
)<(echo 1)
ด้วยลิงก์ไฟล์หลอกโดย/dev/fd
อ้างอิงถึงจุดสิ้นสุดการอ่านของไปป์ที่ไม่มีชื่อ/dev/fd/
มา เนื่องจากตัวบ่งชี้ไฟล์ที่เกี่ยวข้องยังคงเปิดอยู่จึงยังคงสอดคล้องกับจุดสิ้นสุดการอ่านของไพพ์ ดังนั้นหากโปรแกรม PHP เปิดไฟล์ที่กำหนดไว้สำหรับการอ่านสิ่งที่มันทำคือการสร้างsecond
ไฟล์ descriptor สำหรับปลายอ่านของไพพ์ที่ไม่มีชื่อ แต่นั่นก็ไม่มีปัญหามันสามารถอ่านได้จากทั้งสองecho
คำสั่งซึ่งไปยังจุดสิ้นสุดการเขียนของไพพ์เดียวกันการกู้ยืมเงินจาก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
ได้