Bài viết này mô tả việc triển khai các đường ống dẫn trong nhân Unix. Tôi hơi thất vọng vì một bài báo gần đây có tiêu đề "
Chúng ta đang nói về điều gì vậy?
Đường ống "có lẽ là phát minh quan trọng nhất trong Unix" - một đặc điểm xác định triết lý cơ bản của Unix về việc kết hợp các chương trình nhỏ lại với nhau và khẩu hiệu dòng lệnh quen thuộc:
$ echo hello | wc -c
6
Chức năng này phụ thuộc vào lệnh gọi hệ thống do hạt nhân cung cấp pipe
, được mô tả trên các trang tài liệu
Đường ống cung cấp kênh một chiều để liên lạc giữa các quá trình. Đường ống có đầu vào (đầu ghi) và đầu ra (đầu đọc). Dữ liệu được ghi vào đầu vào của đường ống có thể được đọc ở đầu ra.
Đường ống được tạo bằng cách gọi
pipe(2)
, trả về hai bộ mô tả tệp: một bộ mô tả đầu vào của đường ống, bộ thứ hai chỉ đầu ra.
Đầu ra theo dõi từ lệnh trên cho thấy việc tạo một đường ống dẫn và luồng dữ liệu qua nó từ quy trình này sang quy trình khác:
$ strace -qf -e execve,pipe,dup2,read,write
sh -c 'echo hello | wc -c'
execve("/bin/sh", ["sh", "-c", "echo hello | wc -c"], …)
pipe([3, 4]) = 0
[pid 2604795] dup2(4, 1) = 1
[pid 2604795] write(1, "hellon", 6) = 6
[pid 2604796] dup2(3, 0) = 0
[pid 2604796] execve("/usr/bin/wc", ["wc", "-c"], …)
[pid 2604796] read(0, "hellon", 16384) = 6
[pid 2604796] write(1, "6n", 2) = 2
Tiến trình cha gọi pipe()
để có được mô tả tập tin đính kèm. Một tiến trình con ghi vào một bộ mô tả và một tiến trình khác đọc cùng một dữ liệu từ một bộ mô tả khác. Shell "đổi tên" bộ mô tả 2 và 3 bằng dup4 để khớp với stdin và stdout.
Nếu không có đường ống dẫn, trình bao sẽ phải ghi đầu ra của một quy trình vào một tệp và chuyển nó sang một quy trình khác để đọc dữ liệu từ tệp. Kết quả là chúng ta sẽ lãng phí nhiều tài nguyên và dung lượng đĩa hơn. Tuy nhiên, đường ống tốt hơn là chỉ tránh các tệp tạm thời:
Nếu một quá trình cố gắng đọc từ một đường dẫn trống, thì
read(2)
sẽ chặn cho đến khi có dữ liệu. Nếu một quá trình cố ghi vào một đường dẫn đầy đủ, thìwrite(2)
sẽ chặn cho đến khi đọc đủ dữ liệu từ đường ống để hoàn tất quá trình ghi.
Giống như yêu cầu POSIX, đây là một thuộc tính quan trọng: ghi vào đường ống lên tới PIPE_BUF
byte (ít nhất là 512) phải là nguyên tử để các quy trình có thể giao tiếp với nhau thông qua đường ống theo cách mà các tệp thông thường (không cung cấp các đảm bảo như vậy) không thể.
Với một tệp thông thường, một quy trình có thể ghi tất cả đầu ra của nó vào tệp đó và chuyển nó sang một quy trình khác. Hoặc các quy trình có thể hoạt động ở chế độ song song cứng, sử dụng cơ chế báo hiệu bên ngoài (như semaphore) để thông báo cho nhau về việc hoàn thành ghi hoặc đọc. Băng tải cứu chúng ta khỏi tất cả những rắc rối này.
Chúng tôi đang tìm kiếm cái gì?
Tôi sẽ giải thích trên đầu ngón tay để bạn dễ hình dung băng chuyền hoạt động như thế nào. Bạn sẽ cần phân bổ bộ đệm và một số trạng thái trong bộ nhớ. Bạn sẽ cần các hàm để thêm và xóa dữ liệu khỏi bộ đệm. Bạn sẽ cần một số phương tiện để gọi các hàm trong quá trình đọc và ghi trên các bộ mô tả tệp. Và khóa là cần thiết để thực hiện hành vi đặc biệt được mô tả ở trên.
Bây giờ chúng ta đã sẵn sàng thẩm vấn mã nguồn của hạt nhân dưới ánh đèn sáng để xác nhận hoặc bác bỏ mô hình tinh thần mơ hồ của chúng ta. Nhưng hãy luôn chuẩn bị cho những điều bất ngờ.
Chúng ta đang tìm ở đâu?
Tôi không biết cuốn sách nổi tiếng của mình nằm ở đâu.
Lang thang trong kho lưu trữ TUHS giống như đến thăm một viện bảo tàng. Chúng ta có thể xem lại lịch sử được chia sẻ của mình và tôi đánh giá cao nỗ lực trong nhiều năm để khôi phục từng chút một tất cả tài liệu này từ các băng cassette và bản in cũ. Và tôi nhận thức sâu sắc về những mảnh vỡ vẫn còn thiếu.
Sau khi đã thỏa mãn trí tò mò về lịch sử xa xưa của đường ống, chúng ta có thể nhìn vào các lõi hiện đại để so sánh.
Ngẫu nhiên, pipe
là cuộc gọi hệ thống số 42 trong bảng sysent[]
. Sự trùng hợp ngẫu nhiên?
Nhân Unix truyền thống (1970–1974)
Tôi không tìm thấy bất kỳ dấu vết nào pipe(2)
không phải trong
TUHS tuyên bố rằng
Phiên bản thứ ba của Unix là phiên bản cuối cùng có hạt nhân được viết bằng trình biên dịch chương trình hợp ngữ, nhưng cũng là phiên bản đầu tiên có đường ống dẫn. Trong năm 1973, công việc đang được tiến hành để cải thiện phiên bản thứ ba, nhân được viết lại bằng C, và do đó phiên bản thứ tư của Unix ra đời.
Một độc giả đã tìm thấy bản quét của một tài liệu trong đó Doug McIlroy đề xuất ý tưởng "kết nối các chương trình giống như vòi tưới vườn".
Trong cuốn sách của Brian Kernighan
Khi Unix xuất hiện, niềm đam mê của tôi với coroutine đã khiến tôi yêu cầu tác giả hệ điều hành, Ken Thompson, cho phép dữ liệu được ghi vào một quy trình nào đó không chỉ đi đến thiết bị mà còn đi đến lối ra của quy trình khác. Ken quyết định điều đó là có thể. Tuy nhiên, là một người theo chủ nghĩa tối giản, anh ấy muốn mọi tính năng của hệ thống đóng một vai trò quan trọng. Việc ghi trực tiếp giữa các quy trình có thực sự là một lợi thế lớn so với việc ghi vào một tệp trung gian không? Và chỉ khi tôi đưa ra một đề xuất cụ thể với cái tên hấp dẫn "đường ống" và mô tả cú pháp tương tác của các quy trình, Ken cuối cùng mới thốt lên: "Tôi sẽ làm được!".
Và đã làm. Vào một buổi tối định mệnh, Ken đã thay đổi kernel và shell, sửa một số chương trình tiêu chuẩn để chuẩn hóa cách chúng chấp nhận đầu vào (có thể đến từ một đường dẫn) và thay đổi tên tệp. Ngày hôm sau, đường ống được sử dụng rất rộng rãi trong các ứng dụng. Đến cuối tuần, các thư ký sử dụng chúng để gửi tài liệu từ bộ xử lý văn bản đến máy in. Một thời gian sau, Ken đã thay thế API và cú pháp ban đầu để gói gọn việc sử dụng đường ống dẫn bằng các quy ước rõ ràng hơn đã được sử dụng kể từ đó.
Thật không may, mã nguồn cho nhân Unix phiên bản thứ ba đã bị mất. Và mặc dù chúng tôi có mã nguồn hạt nhân được viết bằng C
Chúng tôi có văn bản tài liệu cho pipe(2)
từ cả hai bản phát hành, vì vậy bạn có thể bắt đầu bằng cách tìm kiếm tài liệu pipe(2)
được viết bằng trình biên dịch chương trình hợp ngữ và chỉ trả về một bộ mô tả tệp, nhưng đã cung cấp chức năng cốt lõi dự kiến:
Cuộc gọi hệ thống đường ống tạo ra một cơ chế I/O được gọi là đường ống dẫn. Bộ mô tả tệp được trả về có thể được sử dụng cho các thao tác đọc và ghi. Khi một cái gì đó được ghi vào đường ống, nó sẽ lưu vào bộ đệm tới 504 byte dữ liệu, sau đó quá trình ghi bị tạm dừng. Khi đọc từ đường ống, dữ liệu được lưu trong bộ đệm sẽ được lấy.
Đến năm sau, kernel đã được viết lại bằng C, và pipe(fildes)
»:
Cuộc gọi hệ thống đường ống tạo ra một cơ chế I/O được gọi là đường ống dẫn. Các bộ mô tả tệp được trả về có thể được sử dụng trong các thao tác đọc và ghi. Khi một cái gì đó được ghi vào đường ống, bộ mô tả được trả về trong r1 (resp. fides[1]) được sử dụng, lưu vào bộ đệm tối đa 4096 byte dữ liệu, sau đó quá trình ghi bị tạm dừng. Khi đọc từ đường ống, bộ mô tả được trả về r0 (resp. fides[0]) lấy dữ liệu.
Giả định rằng khi một đường ống đã được xác định, hai (hoặc nhiều) quy trình tương tác (được tạo bởi các yêu cầu tiếp theo ngã ba) sẽ truyền dữ liệu từ đường ống bằng các cuộc gọi đọc и viết.
Shell có một cú pháp để xác định một mảng tuyến tính gồm các quy trình được kết nối thông qua một đường ống dẫn.
Các cuộc gọi để đọc từ một đường ống trống (không chứa dữ liệu được đệm) chỉ có một đầu (tất cả các bộ mô tả tệp ghi đã đóng) trả về "cuối tệp". Viết cuộc gọi trong một tình huống tương tự được bỏ qua.
sớm nhất
Phiên bản thứ sáu của Unix (1975)
Bắt đầu đọc mã nguồn Unix
Trong nhiều năm cuốn sách Lions là tài liệu duy nhất về nhân Unix có sẵn bên ngoài Bell Labs. Mặc dù giấy phép ấn bản thứ sáu cho phép giáo viên sử dụng mã nguồn của nó, nhưng giấy phép ấn bản thứ bảy đã loại trừ khả năng này, vì vậy cuốn sách đã được phân phối dưới dạng bản đánh máy bất hợp pháp.
Ngày nay, bạn có thể mua một bản sao in lại của cuốn sách, trang bìa mô tả các sinh viên tại máy photocopy. Và cảm ơn Warren Toomey (người đã bắt đầu dự án TUHS), bạn có thể tải xuống
Hơn 15 năm trước, tôi đã nhập một bản sao của mã nguồn được cung cấp trong Lionsbởi vì tôi không thích chất lượng bản sao của mình so với vô số bản sao khác. TUHS chưa tồn tại và tôi không có quyền truy cập vào các nguồn cũ. Nhưng vào năm 1988, tôi tìm thấy một cuộn băng cũ có 9 bản nhạc có bản sao lưu từ máy tính PDP11. Thật khó để biết liệu nó có hoạt động hay không, nhưng có một cây /usr/src/ nguyên vẹn trong đó hầu hết các tệp được đánh dấu là năm 1979, thậm chí sau đó trông có vẻ cổ xưa. Tôi nghĩ đó là phiên bản thứ bảy, hoặc một phiên bản phái sinh của PWB.
Tôi lấy phát hiện làm cơ sở và chỉnh sửa thủ công các nguồn thành trạng thái của phiên bản thứ sáu. Một phần của mã vẫn giữ nguyên, một phần phải được chỉnh sửa một chút, thay đổi mã thông báo hiện đại += thành =+ lỗi thời. Một cái gì đó chỉ đơn giản là bị xóa và một cái gì đó phải được viết lại hoàn toàn, nhưng không quá nhiều.
Và hôm nay chúng ta có thể đọc trực tuyến tại TUHS mã nguồn của phiên bản thứ sáu của
Nhân tiện, thoạt nhìn, tính năng chính của mã C trước thời kỳ Kernighan và Ritchie là nó sự ngắn gọn. Tôi không thường xuyên có thể chèn các đoạn mã mà không cần chỉnh sửa nhiều để phù hợp với khu vực hiển thị tương đối hẹp trên trang web của mình.
Đầu
/*
* Max allowable buffering per pipe.
* This is also the max size of the
* file created to implement the pipe.
* If this size is bigger than 4096,
* pipes will be implemented in LARG
* files, which is probably not good.
*/
#define PIPSIZ 4096
Kích thước bộ đệm không thay đổi kể từ lần xuất bản thứ tư. Nhưng ở đây chúng ta thấy, không có bất kỳ tài liệu công khai nào, rằng các đường dẫn đã từng sử dụng các tệp làm bộ lưu trữ dự phòng!
Đối với các tệp LARG, chúng tương ứng với
Đây là cuộc gọi hệ thống thực sự pipe
:
/*
* The sys-pipe entry.
* Allocate an inode on the root device.
* Allocate 2 file structures.
* Put it all together with flags.
*/
pipe()
{
register *ip, *rf, *wf;
int r;
ip = ialloc(rootdev);
if(ip == NULL)
return;
rf = falloc();
if(rf == NULL) {
iput(ip);
return;
}
r = u.u_ar0[R0];
wf = falloc();
if(wf == NULL) {
rf->f_count = 0;
u.u_ofile[r] = NULL;
iput(ip);
return;
}
u.u_ar0[R1] = u.u_ar0[R0]; /* wf's fd */
u.u_ar0[R0] = r; /* rf's fd */
wf->f_flag = FWRITE|FPIPE;
wf->f_inode = ip;
rf->f_flag = FREAD|FPIPE;
rf->f_inode = ip;
ip->i_count = 2;
ip->i_flag = IACC|IUPD;
ip->i_mode = IALLOC;
}
Nhận xét mô tả rõ ràng những gì đang xảy ra ở đây. Nhưng không dễ để hiểu mã, một phần là do cách "R0
и R1
tham số cuộc gọi hệ thống và giá trị trả về được thông qua.
Hãy thử với
pipe()
đến hạn R0
и R1
trả lại số mô tả tệp để đọc và viết. falloc()
trả về một con trỏ tới cấu trúc tệp, nhưng cũng "trả về" thông qua u.u_ar0[R0]
và một bộ mô tả tập tin. Đó là, mã được lưu trữ trong r
bộ mô tả tệp để đọc và gán một bộ mô tả để ghi trực tiếp từ u.u_ar0[R0]
sau cuộc gọi thứ hai falloc()
.
Флаг FPIPE
, mà chúng tôi đặt khi tạo đường ống, kiểm soát hành vi của chức năng
/*
* common code for read and write calls:
* check permissions, set base, count, and offset,
* and switch out to readi, writei, or pipe code.
*/
rdwr(mode)
{
register *fp, m;
m = mode;
fp = getf(u.u_ar0[R0]);
/* … */
if(fp->f_flag&FPIPE) {
if(m==FREAD)
readp(fp); else
writep(fp);
}
/* … */
}
Sau đó chức năng readp()
в pipe.c
đọc dữ liệu từ đường ống. Nhưng tốt hơn là theo dõi việc thực hiện bắt đầu từ writep()
. Một lần nữa, mã đã trở nên phức tạp hơn do bản chất của quy ước chuyển đối số, nhưng một số chi tiết có thể được bỏ qua.
writep(fp)
{
register *rp, *ip, c;
rp = fp;
ip = rp->f_inode;
c = u.u_count;
loop:
/* If all done, return. */
plock(ip);
if(c == 0) {
prele(ip);
u.u_count = 0;
return;
}
/*
* If there are not both read and write sides of the
* pipe active, return error and signal too.
*/
if(ip->i_count < 2) {
prele(ip);
u.u_error = EPIPE;
psignal(u.u_procp, SIGPIPE);
return;
}
/*
* If the pipe is full, wait for reads to deplete
* and truncate it.
*/
if(ip->i_size1 == PIPSIZ) {
ip->i_mode =| IWRITE;
prele(ip);
sleep(ip+1, PPIPE);
goto loop;
}
/* Write what is possible and loop back. */
u.u_offset[0] = 0;
u.u_offset[1] = ip->i_size1;
u.u_count = min(c, PIPSIZ-u.u_offset[1]);
c =- u.u_count;
writei(ip);
prele(ip);
if(ip->i_mode&IREAD) {
ip->i_mode =& ~IREAD;
wakeup(ip+2);
}
goto loop;
}
Chúng tôi muốn ghi byte vào đầu vào đường ống u.u_count
. Đầu tiên chúng ta cần khóa inode (xem bên dưới plock
/prele
).
Sau đó, chúng tôi kiểm tra số lượng tham chiếu inode. Miễn là cả hai đầu của đường ống vẫn mở, bộ đếm phải là 2. Chúng tôi giữ một liên kết (từ rp->f_inode
), vì vậy nếu bộ đếm nhỏ hơn 2, thì điều này có nghĩa là quá trình đọc đã đóng phần cuối của đường ống. Nói cách khác, chúng tôi đang cố ghi vào một đường dẫn kín, đó là một sai lầm. Mã lỗi đầu tiên EPIPE
và tín hiệu SIGPIPE
xuất hiện trong phiên bản thứ sáu của Unix.
Nhưng ngay cả khi băng tải mở, nó có thể đầy. Trong trường hợp này, chúng tôi giải phóng khóa và đi ngủ với hy vọng rằng một quy trình khác sẽ đọc từ đường ống và giải phóng đủ dung lượng trong đó. Khi thức dậy, chúng ta quay lại từ đầu, treo khóa lại và bắt đầu một chu kỳ ghi mới.
Nếu có đủ dung lượng trống trong đường ống, thì chúng tôi ghi dữ liệu vào đó bằng cách sử dụng i_size1
inode'a (với một đường dẫn trống có thể bằng 0) trỏ đến phần cuối của dữ liệu mà nó đã chứa. Nếu có đủ chỗ để viết, chúng ta có thể lấp đầy đường ống dẫn từ i_size1
để PIPESIZ
. Sau đó, chúng tôi giải phóng khóa và cố gắng đánh thức bất kỳ quy trình nào đang chờ đọc từ đường ống dẫn. Chúng tôi quay lại từ đầu để xem liệu chúng tôi có ghi được nhiều byte như chúng tôi cần hay không. Nếu không, thì chúng tôi bắt đầu một chu kỳ ghi mới.
Thông thường tham số i_mode
inode được sử dụng để lưu trữ quyền r
, w
и x
. Nhưng trong trường hợp đường ống, chúng tôi báo hiệu rằng một số quy trình đang chờ ghi hoặc đọc bằng cách sử dụng các bit IREAD
и IWRITE
tương ứng. Quá trình đặt cờ và gọi sleep()
, và dự kiến trong tương lai một số tiến trình khác sẽ gọi wakeup()
.
Điều kỳ diệu thực sự xảy ra trong sleep()
и wakeup()
. Chúng được thực hiện trong
/*
* Give up the processor till a wakeup occurs
* on chan, at which time the process
* enters the scheduling queue at priority pri.
* The most important effect of pri is that when
* pri<0 a signal cannot disturb the sleep;
* if pri>=0 signals will be processed.
* Callers of this routine must be prepared for
* premature return, and check that the reason for
* sleeping has gone away.
*/
sleep(chan, pri) /* … */
/*
* Wake up all processes sleeping on chan.
*/
wakeup(chan) /* … */
Quá trình gọi sleep()
đối với một kênh cụ thể, sau đó có thể được đánh thức bởi một quy trình khác, quy trình này sẽ gọi wakeup()
cho cùng một kênh. writep()
и readp()
phối hợp hành động của họ thông qua các cuộc gọi được ghép nối như vậy. lưu ý rằng pipe.c
luôn ưu tiên PPIPE
khi được gọi sleep()
, vì vậy tất cả sleep()
có thể bị gián đoạn bởi một tín hiệu.
Bây giờ chúng ta có mọi thứ để hiểu chức năng readp()
:
readp(fp)
int *fp;
{
register *rp, *ip;
rp = fp;
ip = rp->f_inode;
loop:
/* Very conservative locking. */
plock(ip);
/*
* If the head (read) has caught up with
* the tail (write), reset both to 0.
*/
if(rp->f_offset[1] == ip->i_size1) {
if(rp->f_offset[1] != 0) {
rp->f_offset[1] = 0;
ip->i_size1 = 0;
if(ip->i_mode&IWRITE) {
ip->i_mode =& ~IWRITE;
wakeup(ip+1);
}
}
/*
* If there are not both reader and
* writer active, return without
* satisfying read.
*/
prele(ip);
if(ip->i_count < 2)
return;
ip->i_mode =| IREAD;
sleep(ip+2, PPIPE);
goto loop;
}
/* Read and return */
u.u_offset[0] = 0;
u.u_offset[1] = rp->f_offset[1];
readi(ip);
rp->f_offset[1] = u.u_offset[1];
prele(ip);
}
Bạn có thể thấy dễ dàng hơn khi đọc chức năng này từ dưới lên trên. Nhánh "đọc và trả về" thường được sử dụng khi có một số dữ liệu trong đường dẫn. Trong trường hợp này, chúng tôi sử dụng f_offset
đọc, sau đó cập nhật giá trị của phần bù tương ứng.
Trong các lần đọc tiếp theo, đường ống sẽ trống nếu đã đạt đến độ lệch đọc i_size1
tại nút. Chúng tôi đặt lại vị trí về 0 và cố gắng đánh thức bất kỳ quy trình nào muốn ghi vào đường ống dẫn. Chúng tôi biết rằng khi băng tải đầy, writep()
ngủ thiếp đi ip+1
. Và bây giờ khi đường ống trống, chúng ta có thể đánh thức nó để tiếp tục chu kỳ ghi của nó.
Nếu không có gì để đọc, thì readp()
có thể đặt cờ IREAD
và chìm vào giấc ngủ ip+2
. Chúng tôi biết điều gì sẽ đánh thức anh ấy writep()
khi nó ghi một số dữ liệu vào đường ống.
Nhận xét về u
» chúng ta có thể coi chúng như các hàm I/O thông thường lấy một tệp, một vị trí, một vùng đệm trong bộ nhớ và đếm số byte cần đọc hoặc ghi.
/*
* Read the file corresponding to
* the inode pointed at by the argument.
* The actual read arguments are found
* in the variables:
* u_base core address for destination
* u_offset byte offset in file
* u_count number of bytes to read
* u_segflg read to kernel/user
*/
readi(aip)
struct inode *aip;
/* … */
/*
* Write the file corresponding to
* the inode pointed at by the argument.
* The actual write arguments are found
* in the variables:
* u_base core address for source
* u_offset byte offset in file
* u_count number of bytes to write
* u_segflg write to kernel/user
*/
writei(aip)
struct inode *aip;
/* … */
Đối với việc chặn "bảo thủ", thì readp()
и writep()
khóa các nút cho đến khi chúng kết thúc hoặc nhận được kết quả (tức là gọi wakeup
). plock()
и prele()
hoạt động đơn giản: sử dụng một nhóm cuộc gọi khác sleep
и wakeup
cho phép chúng tôi đánh thức bất kỳ quy trình nào cần khóa mà chúng tôi vừa phát hành:
/*
* Lock a pipe.
* If its already locked, set the WANT bit and sleep.
*/
plock(ip)
int *ip;
{
register *rp;
rp = ip;
while(rp->i_flag&ILOCK) {
rp->i_flag =| IWANT;
sleep(rp, PPIPE);
}
rp->i_flag =| ILOCK;
}
/*
* Unlock a pipe.
* If WANT bit is on, wakeup.
* This routine is also used to unlock inodes in general.
*/
prele(ip)
int *ip;
{
register *rp;
rp = ip;
rp->i_flag =& ~ILOCK;
if(rp->i_flag&IWANT) {
rp->i_flag =& ~IWANT;
wakeup(rp);
}
}
Lúc đầu tôi không thể hiểu tại sao readp()
không gây ra prele(ip)
trước cuộc gọi wakeup(ip+1)
. Điều đầu tiên writep()
gọi trong vòng lặp của nó, điều này plock(ip)
, dẫn đến bế tắc nếu readp()
vẫn chưa xóa khối của nó, vì vậy mã phải hoạt động chính xác bằng cách nào đó. Nếu bạn nhìn vào wakeup()
, nó trở nên rõ ràng rằng nó chỉ đánh dấu quá trình ngủ là đã sẵn sàng để thực thi, để trong tương lai sched()
thực sự đưa ra nó. Vì thế readp()
nguyên nhân wakeup()
, mở khóa, bộ IREAD
và cuộc gọi sleep(ip+2)
- tất cả điều này trước đây writep()
khởi động lại chu kỳ.
Điều này hoàn thành mô tả về các đường ống trong phiên bản thứ sáu. Mã đơn giản, ý nghĩa sâu rộng.
Xv6, một hạt nhân giống Unix đơn giản
Để tạo hạt nhân
Mã chứa một triển khai rõ ràng và chu đáo pipealloc()
:
#define PIPESIZE 512
struct pipe {
struct spinlock lock;
char data[PIPESIZE];
uint nread; // number of bytes read
uint nwrite; // number of bytes written
int readopen; // read fd is still open
int writeopen; // write fd is still open
};
int
pipealloc(struct file **f0, struct file **f1)
{
struct pipe *p;
p = 0;
*f0 = *f1 = 0;
if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0)
goto bad;
if((p = (struct pipe*)kalloc()) == 0)
goto bad;
p->readopen = 1;
p->writeopen = 1;
p->nwrite = 0;
p->nread = 0;
initlock(&p->lock, "pipe");
(*f0)->type = FD_PIPE;
(*f0)->readable = 1;
(*f0)->writable = 0;
(*f0)->pipe = p;
(*f1)->type = FD_PIPE;
(*f1)->readable = 0;
(*f1)->writable = 1;
(*f1)->pipe = p;
return 0;
bad:
if(p)
kfree((char*)p);
if(*f0)
fileclose(*f0);
if(*f1)
fileclose(*f1);
return -1;
}
pipealloc()
đặt trạng thái của tất cả phần còn lại của quá trình triển khai, bao gồm các chức năng piperead()
, pipewrite()
и pipeclose()
. Cuộc gọi hệ thống thực tế sys_pipe
là một trình bao bọc được triển khai trong
Linux 0.01
Bạn có thể tìm mã nguồn cho Linux 0.01. Sẽ rất hữu ích khi nghiên cứu việc triển khai các đường ống trong fs
/pipe.c
. Ở đây, một inode được sử dụng để đại diện cho đường ống, nhưng bản thân đường ống được viết bằng ngôn ngữ C hiện đại. Nếu bạn đã đột nhập qua mã phiên bản thứ sáu, bạn sẽ không gặp bất kỳ rắc rối nào ở đây. Đây là những gì chức năng trông giống như write_pipe()
:
int write_pipe(struct m_inode * inode, char * buf, int count)
{
char * b=buf;
wake_up(&inode->i_wait);
if (inode->i_count != 2) { /* no readers */
current->signal |= (1<<(SIGPIPE-1));
return -1;
}
while (count-->0) {
while (PIPE_FULL(*inode)) {
wake_up(&inode->i_wait);
if (inode->i_count != 2) {
current->signal |= (1<<(SIGPIPE-1));
return b-buf;
}
sleep_on(&inode->i_wait);
}
((char *)inode->i_size)[PIPE_HEAD(*inode)] =
get_fs_byte(b++);
INC_PIPE( PIPE_HEAD(*inode) );
wake_up(&inode->i_wait);
}
wake_up(&inode->i_wait);
return b-buf;
}
Ngay cả khi không nhìn vào các định nghĩa cấu trúc, bạn có thể tìm ra cách số lượng tham chiếu inode được sử dụng để kiểm tra xem thao tác ghi có dẫn đến SIGPIPE
. Ngoài công việc từng byte, chức năng này rất dễ so sánh với các ý tưởng trên. logic chẵn sleep_on
/wake_up
trông không quá xa lạ.
Hạt nhân Linux hiện đại, FreeBSD, NetBSD, OpenBSD
Tôi nhanh chóng lướt qua một số hạt nhân hiện đại. Không ai trong số họ đã triển khai dựa trên đĩa (không có gì đáng ngạc nhiên). Linux có triển khai riêng. Và mặc dù ba hạt nhân BSD hiện đại chứa các triển khai dựa trên mã do John Dyson viết, nhưng qua nhiều năm, chúng đã trở nên quá khác biệt với nhau.
Đọc fs
/pipe.c
(trên Linux) hoặc sys
/kern
/sys_pipe.c
(trên *BSD), nó đòi hỏi sự cống hiến thực sự. Hiệu suất và hỗ trợ cho các tính năng như véc tơ và I/O không đồng bộ rất quan trọng trong mã ngày nay. Và các chi tiết về cấp phát bộ nhớ, khóa và cấu hình kernel đều khác nhau rất nhiều. Đây không phải là những gì các trường đại học cần cho một khóa học nhập môn về hệ điều hành.
Trong mọi trường hợp, thật thú vị đối với tôi khi khai quật được một số mẫu cũ (ví dụ: tạo SIGPIPE
và quay lại EPIPE
khi ghi vào một đường dẫn kín) trong tất cả các hạt nhân hiện đại, rất khác biệt này. Tôi có thể sẽ không bao giờ nhìn thấy một chiếc máy tính PDP-11 hoạt động, nhưng vẫn còn rất nhiều điều cần học hỏi từ đoạn mã được viết vài năm trước khi tôi ra đời.
Được viết bởi Divi Kapoor vào năm 2011, bài báo "
Nguồn: www.habr.com