Lưu ý: Câu trả lời phản ánh sự hiểu biết của tôi về các cơ chế này cho đến nay, được tích lũy qua nghiên cứu và đọc câu trả lời của các đồng nghiệp trên trang web này và unix.stackexchange.com , và sẽ được cập nhật khi có thời gian. Đừng ngần ngại đặt câu hỏi hoặc đề xuất cải tiến trong các ý kiến. Tôi cũng đề nghị bạn thử xem các tòa nhà chọc trời hoạt động như thế nào trong shell với strace
lệnh. Ngoài ra, vui lòng không bị đe dọa bởi khái niệm bên trong hoặc tòa nhà - bạn không cần phải biết hoặc có thể sử dụng chúng để hiểu cách vỏ làm mọi thứ, nhưng chúng chắc chắn giúp hiểu.
TL; DR
|
các đường ống không được liên kết với một mục trên đĩa, do đó không có số lượng hệ thống tập tin đĩa inode (nhưng có inode trong hệ thống tập tin ảo pipefs trong không gian kernel), nhưng chuyển hướng thường liên quan đến các tập tin, có mục nhập đĩa và do đó có tương ứng inode.
- các đường ống không
lseek()
có khả năng để các lệnh không thể đọc một số dữ liệu và sau đó tua lại, nhưng khi bạn chuyển hướng với >
hoặc <
thường là một tệp lseek()
có thể là đối tượng, vì vậy các lệnh có thể điều hướng theo ý muốn.
- chuyển hướng là các thao tác trên mô tả tập tin, có thể nhiều; ống chỉ có hai mô tả tập tin - một cho lệnh trái và một cho lệnh phải
- chuyển hướng trên các luồng tiêu chuẩn và đường ống đều được đệm.
- ống hầu như luôn luôn liên quan đến việc rèn và do đó các cặp quy trình có liên quan; chuyển hướng - không phải luôn luôn, mặc dù trong cả hai trường hợp, mô tả tệp kết quả được kế thừa bởi các quy trình phụ.
- ống luôn kết nối bộ mô tả tệp (một cặp), chuyển hướng - hoặc sử dụng tên đường dẫn hoặc mô tả tệp.
- các đường ống là phương thức Giao tiếp giữa các quá trình, trong khi các chuyển hướng chỉ là các thao tác trên các tệp đang mở hoặc các đối tượng giống như tệp
- cả hai đều sử dụng
dup2()
các tòa nhà cao tầng bên dưới mui xe để cung cấp các bản sao mô tả tệp, nơi xảy ra luồng dữ liệu thực tế.
- chuyển hướng có thể được áp dụng "toàn cầu" với
exec
lệnh tích hợp (xem cái này và cái này ), vì vậy nếu bạn thực hiện thì exec > output.txt
mọi lệnh sẽ ghi vào output.txt
từ đó trở đi. |
ống chỉ được áp dụng cho lệnh hiện tại (có nghĩa là lệnh đơn giản hoặc lệnh con giống seq 5 | (head -n1; head -n2)
hoặc lệnh ghép.
Khi chuyển hướng được thực hiện trên các tệp, những thứ như echo "TEST" > file
và echo "TEST" >> file
cả hai đều sử dụng hình chữ nhật open()
trên tệp đó ( xem thêm ) và lấy mô tả tệp từ nó để chuyển nó đến dup2()
. Ống |
chỉ sử dụng pipe()
và dup2()
tòa nhà.
Theo như các lệnh được thực thi, các đường ống và chuyển hướng không nhiều hơn các mô tả tệp - các đối tượng giống như tệp mà chúng có thể viết một cách mù quáng hoặc thao túng chúng bên trong (có thể tạo ra các hành vi không mong muốn; apt
ví dụ, có xu hướng thậm chí không ghi vào thiết bị xuất chuẩn nếu nó biết có sự chuyển hướng).
Giới thiệu
Để hiểu hai cơ chế này khác nhau như thế nào, cần phải hiểu các thuộc tính thiết yếu của chúng, lịch sử đằng sau hai cơ chế và nguồn gốc của chúng trong ngôn ngữ lập trình C. Trong thực tế, biết mô tả tập tin là gì, và cách thức dup2()
và pipe()
các cuộc gọi hệ thống hoạt động là điều cần thiết, cũng như lseek()
. Shell có nghĩa là một cách làm cho các cơ chế này trở nên trừu tượng với người dùng, nhưng đào sâu hơn sự trừu tượng giúp hiểu bản chất thực sự của hành vi của shell.
Nguồn gốc của chuyển hướng và đường ống
Theo bài báo Prophetic Petroglyphs của Dennis Ritche , các đường ống có nguồn gốc từ một bản ghi nhớ nội bộ năm 1964 của Malcolm Douglas McIlroy , tại thời điểm họ đang làm việc trên hệ điều hành Multics . Trích dẫn:
Để đặt những mối quan tâm mạnh mẽ nhất của tôi vào một tóm tắt:
- Chúng ta nên có một số cách kết nối các chương trình như vòi vườn - vít ở một phân khúc khác khi nó trở nên cần thiết để xoa bóp dữ liệu theo cách khác. Đây cũng là cách của IO.
Điều rõ ràng là vào thời điểm đó, các chương trình có khả năng ghi vào đĩa, tuy nhiên điều đó không hiệu quả nếu đầu ra lớn. Để trích dẫn lời giải thích của Brian Kernighan trong video Unix Pipeline :
Trước tiên, bạn không phải viết một chương trình lớn - bạn đã có các chương trình nhỏ hơn hiện có thể thực hiện các phần của công việc ... Một điều nữa là có thể lượng dữ liệu bạn cung cấp sẽ không phù hợp nếu bạn đã lưu nó trong một tệp ... bởi vì hãy nhớ rằng, chúng ta sẽ quay lại thời mà đĩa trên những thứ này có, nếu bạn may mắn, một Megabyte hoặc hai dữ liệu ... Vì vậy, đường ống không bao giờ phải khởi tạo toàn bộ đầu ra .
Do đó, sự khác biệt về khái niệm là rõ ràng: đường ống là một cơ chế làm cho các chương trình nói chuyện với nhau. Chuyển hướng - là cách viết để nộp ở mức cơ bản. Trong cả hai trường hợp, shell làm cho hai điều này trở nên dễ dàng, nhưng bên dưới mui xe, có rất nhiều điều đang diễn ra.
Đi sâu hơn: các tòa nhà cao tầng và hoạt động bên trong của vỏ
Chúng tôi bắt đầu với khái niệm mô tả tập tin . Mô tả tệp mô tả về cơ bản một tệp đang mở (cho dù đó là tệp trên đĩa, hoặc trong bộ nhớ hoặc tệp ẩn danh), được biểu thị bằng một số nguyên. Hai luồng dữ liệu tiêu chuẩn (stdin, stdout, stderr) lần lượt là các mô tả tệp 0,1 và 2. Họ đến từ đâu ? Vâng, trong các lệnh shell, các bộ mô tả tệp được kế thừa từ shell cha của chúng. Và nó nói chung đúng cho tất cả các quy trình - quy trình con kế thừa các mô tả tệp của cha mẹ. Đối với trình tiện ích, thông thường đóng tất cả các mô tả tệp được kế thừa và / hoặc chuyển hướng đến các vị trí khác.
Quay lại chuyển hướng. Nó thực sự là gì? Đó là một cơ chế yêu cầu shell chuẩn bị các mô tả tệp cho lệnh (vì việc chuyển hướng được thực hiện bởi shell trước khi lệnh chạy) và chỉ ra chúng ở nơi người dùng đề xuất. Các định nghĩa tiêu chuẩn của chuyển hướng đầu ra là
[n]>word
Đó [n]
là số mô tả tập tin. Khi bạn làm echo "Something" > /dev/null
số 1 được ngụ ý ở đó, và echo 2> /dev/null
.
Bên dưới mui xe, việc này được thực hiện bằng cách sao chép mô tả tệp qua dup2()
lệnh gọi hệ thống. Hãy lấy đi df > /dev/null
. Shell sẽ tạo ra một tiến trình con khi df
chạy, nhưng trước đó nó sẽ mở /dev/null
dưới dạng mô tả tệp # 3 và dup2(3,1)
sẽ được phát hành, tạo ra một bản sao của mô tả tệp 3 và bản sao sẽ là 1. Bạn biết cách bạn có hai tệp file1.txt
và file2.txt
và khi bạn cp file1.txt file2.txt
có hai tệp giống nhau, nhưng bạn có thể thao tác chúng một cách độc lập không? Đó là điều tương tự xảy ra ở đây. Thường thì bạn có thể thấy rằng trước khi chạy, bash
sẽ làm dup(1,10)
một bản mô tả tệp sao chép số 1 stdout
(và bản sao đó sẽ là fd # 10) để khôi phục lại sau này. Quan trọng là lưu ý rằng khi bạn xem xét các lệnh tích hợp(là một phần của chính shell và không có tệp trong /bin
hoặc ở nơi khác) hoặc các lệnh đơn giản trong shell không tương tác , shell không tạo ra một tiến trình con.
Và sau đó chúng ta có những thứ như [n]>&[m]
và [n]&<[m]
. Đây là sao chép mô tả tệp, cơ chế giống như dup2()
bây giờ trong cú pháp shell, có sẵn thuận tiện cho người dùng.
Một trong những điều quan trọng cần lưu ý về chuyển hướng là thứ tự của chúng không cố định, nhưng rất quan trọng đối với cách shell diễn giải những gì người dùng muốn. So sánh như sau:
# Make copy of where fd 2 points , then redirect fd 2
$ ls -l /proc/self/fd/ 3>&2 2> /dev/null
total 0
lrwx------ 1 user user 64 Sep 13 00:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Sep 13 00:08 1 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:08 2 -> /dev/null
lrwx------ 1 runner user 64 Sep 13 00:08 3 -> /dev/pts/0
lr-x------ 1 user user 64 Sep 13 00:08 4 -> /proc/29/fd
# redirect fd #2 first, then clone it
$ ls -l /proc/self/fd/ 2> /dev/null 3>&2
total 0
lrwx------ 1 user user 64 Sep 13 00:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Sep 13 00:08 1 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:08 2 -> /dev/null
l-wx------ 1 user user 64 Sep 13 00:08 3 -> /dev/null
lr-x------ 1 user user 64 Sep 13 00:08 4 -> /proc/31/fd
Việc sử dụng thực tế những điều này trong kịch bản shell có thể linh hoạt:
và nhiều thứ khác.
Hệ thống nước với pipe()
vàdup2()
Vậy làm thế nào để đường ống được tạo ra? Via pipe()
syscall , mà sẽ mất đầu vào một mảng (hay còn gọi là danh sách) gọi là pipefd
hai mặt hàng các loại int
(số nguyên). Hai số nguyên này là mô tả tập tin. Các pipefd[0]
sẽ được kết thúc đọc của ống và pipefd[1]
sẽ là ghi kết thúc. Vì vậy, trong df | grep 'foo'
, grep
sẽ nhận được bản sao pipefd[0]
và df
sẽ nhận được một bản sao của pipefd[1]
. Nhưng bằng cách nào ? Tất nhiên, với sự kỳ diệu của tòa nhà dup2()
chọc trời. Trong df
ví dụ của chúng tôi, giả sử pipefd[1]
có số 4, vì vậy, vỏ sẽ tạo ra một đứa trẻ, làm dup2(4,1)
(nhớ cp
ví dụ của tôi ?), Và sau đó làm execve()
để thực sự chạy df
. Một cách tự nhiên,df
sẽ kế thừa mô tả tệp số 1, nhưng sẽ không biết rằng nó không còn trỏ đến thiết bị đầu cuối, mà thực sự là fd # 4, thực sự là đầu ghi của đường ống. Đương nhiên, điều tương tự sẽ xảy ra với grep 'foo'
ngoại trừ với số lượng mô tả tệp khác nhau.
Bây giờ, câu hỏi thú vị: chúng ta có thể tạo đường ống chuyển hướng fd # 2 không, không chỉ fd # 1? Vâng, trong thực tế đó là những gì |&
làm trong bash. Các tiêu chuẩn POSIX đòi hỏi ngôn ngữ lệnh shell để hỗ trợ df 2>&1 | grep 'foo'
cú pháp cho mục đích đó, nhưng bash
không |&
là tốt.
Điều quan trọng cần lưu ý là các đường ống luôn luôn xử lý các mô tả tập tin. Có tồn tại FIFO
hoặc tên ống , có tên tệp trên đĩa và cho phép bạn sử dụng nó như một tệp, nhưng hoạt động như một đường ống. Nhưng các |
loại ống là những gì được gọi là ống ẩn danh - chúng không có tên tệp, vì chúng thực sự chỉ là hai đối tượng được kết nối với nhau. Việc chúng ta không xử lý các tệp cũng tạo ra một hàm ý quan trọng: đường ống không lseek()
thể thực hiện được. Các tệp, trong bộ nhớ hoặc trên đĩa, là tĩnh - các chương trình có thể sử dụng lseek()
syscall để chuyển đến byte 120, sau đó quay lại byte 10, sau đó chuyển tiếp đến hết. Các đường ống không tĩnh - chúng tuần tự và do đó bạn không thể tua lại dữ liệu bạn nhận được từ chúng bằnglseek()
. Đây là điều làm cho một số chương trình nhận biết nếu họ đang đọc từ tệp hoặc từ đường ống, và do đó họ có thể thực hiện các điều chỉnh cần thiết để thực hiện hiệu quả; nói cách khác, a prog
có thể phát hiện nếu tôi làm cat file.txt | prog
hoặc prog < input.txt
. Ví dụ công việc thực sự của đó là đuôi .
Hai thuộc tính rất thú vị khác của các đường ống là chúng có bộ đệm, trên Linux là 4096 byte và chúng thực sự có một hệ thống tệp như được định nghĩa trong mã nguồn Linux ! Chúng không chỉ đơn giản là một đối tượng để truyền dữ liệu xung quanh, mà chúng còn là cơ sở hạ tầng! Trong thực tế, vì tồn tại hệ thống tập tin pipefs, quản lý cả hai đường ống và FIFO, các đường ống có số inode trên hệ thống tập tin tương ứng của chúng:
# Stdout of ls is wired to pipe
$ ls -l /proc/self/fd/ | cat
lrwx------ 1 user user 64 Sep 13 00:02 0 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:02 1 -> pipe:[15655630]
lrwx------ 1 user user 64 Sep 13 00:02 2 -> /dev/pts/0
lr-x------ 1 user user 64 Sep 13 00:02 3 -> /proc/22/fd
# stdin of ls is wired to pipe
$ true | ls -l /proc/self/fd/0
lr-x------ 1 user user 64 Sep 13 03:58 /proc/self/fd/0 -> 'pipe:[54741]'
Trên các ống Linux là một hướng, giống như chuyển hướng. Trên một số triển khai giống như Unix - có các đường ống hai chiều. Mặc dù với phép thuật của kịch bản shell, bạn cũng có thể tạo các đường ống hai chiều trên Linux .
Xem thêm:
thing1 > temp_file && thing2 < temp_file
để làm dễ dàng hơn với đường ống. Nhưng tại sao không sử dụng lại>
toán tử để làm điều này, ví dụthing1 > thing2
cho các lệnhthing1
vàthing2
? Tại sao một nhà điều hành thêm|
?