Cách đường ống được triển khai trong Unix

Cách đường ống được triển khai trong Unix
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 đề "Làm thế nào để đường ống làm việc trong Unix?" trở ra ngoai không về cấu trúc bên trong. Tôi tò mò và tìm hiểu các nguồn cũ để tìm câu trả lời.

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 (7) и ống (2):

Đườ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.sách sư tử« với mã nguồn Unix 6, nhưng nhờ Hiệp hội Di sản Unix có thể được tìm kiếm trực tuyến mã nguồn thậm chí các phiên bản cũ hơn của Unix.

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 PDP-7 Unix (tháng 1970 năm XNUMX), cũng không phải trong phiên bản đầu tiên Unix (tháng 1971 năm XNUMX), cũng như trong mã nguồn không đầy đủ Phiên bản thứ hai (tháng 1972 năm XNUMX).

TUHS tuyên bố rằng phiên bản thứ ba Unix (tháng 1973 năm XNUMX) là phiên bản đầu tiên có đường dẫn:

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".

Cách đường ống được triển khai trong Unix
Trong cuốn sách của Brian KernighanUnix: Lịch sử và Hồi ức”, lịch sử xuất hiện của băng tải cũng đề cập đến tài liệu này: “... nó được treo trên tường trong văn phòng của tôi tại Bell Labs trong 30 năm.” Đây phỏng vấn McIlroyvà một câu chuyện khác từ Tác phẩm của McIlroy, được viết vào năm 2014:

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 ấn bản thứ tư, được phát hành vào tháng 1973 năm XNUMX, nhưng nó được phát hành vài tháng trước khi phát hành chính thức và không chứa việc thực hiện các đường ống dẫn. Thật đáng tiếc khi mã nguồn của tính năng Unix huyền thoại này đã bị mất, có lẽ là mãi mãi.

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 ấn bản thứ ba (đối với một số từ nhất định, được gạch dưới "thủ công", một chuỗi ký tự ^H theo sau là dấu gạch dưới!). nguyên mẫu nàypipe(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à ống (2) phiên bản thứ tư có được vẻ ngoài hiện đại với nguyên mẫu "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 thực hiện đường ống được bảo tồn lo ngại đến phiên bản thứ năm của Unix (tháng 1974 năm XNUMX), nhưng nó gần giống với cái xuất hiện trong bản phát hành tiếp theo. Chỉ thêm phần chú thích nên có thể bỏ qua lần xuất bản thứ năm.

Phiên bản thứ sáu của Unix (1975)

Bắt đầu đọc mã nguồn Unix ấn bản thứ sáu (tháng 1975 năm XNUMX). Phần lớn là nhờ Lions nó dễ tìm hơn nhiều so với nguồn của các phiên bản trước:

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 Phiên bản thứ sáu Nguồn PDF. Tôi muốn cho bạn biết bao nhiêu nỗ lực đã bỏ ra để tạo tệp:

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 kho lưu trữ, mà Dennis Ritchie đã nhúng tay vào.

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 /usr/sys/ken/pipe.c có một bình luận giải thích (và vâng, có nhiều hơn nữa /usr/sys/dmr):

/*
 * 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 cờ inode LỚN, được sử dụng bởi "thuật toán địa chỉ lớn" để xử lý khối gián tiếp để hỗ trợ các hệ thống tập tin lớn hơn. Vì Ken nói rằng tốt hơn hết là không nên sử dụng chúng, tôi rất vui khi tin lời anh ấy.

Đâ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 "cấu trúc người dùng u» và đăng ký 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 ialloc() đặt trên đĩa nút (inode)và với sự giúp đỡ Falloc() - cửa hàng hai tập tin. Nếu mọi việc suôn sẻ, chúng tôi sẽ đặt cờ để xác định các tệp này là hai đầu của đường ống, trỏ chúng đến cùng một inode (có số tham chiếu trở thành 2) và đánh dấu inode là đã sửa đổi và đang sử dụng. Chú ý đến các yêu cầu để tôi đặt() trong các đường dẫn lỗi để giảm số lượng tham chiếu trong nút mớ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 rdwr() trong sys2.c, gọi các thủ tục I/O cụ thể:

/*
 * 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 viết()... Tham số 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 slp.c, nguồn gốc của bình luận nổi tiếng "Bạn không được mong đợi để hiểu điều này". May mắn thay, chúng ta không cần phải hiểu mã, chỉ cần xem một số nhận xét:

/*
 * 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 đọc() đọc càng nhiều dữ liệu càng tốt bắt đầu từ dữ liệu hiện tại 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ề read() và writei() sẽ giúp bạn hiểu rằng thay vì truyền tham số qua "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.

Phiên bản thứ bảy Unix (tháng 1979 năm XNUMX) là một bản phát hành chính mới (bốn năm sau) giới thiệu nhiều ứng dụng và tính năng nhân mới. Nó cũng đã trải qua những thay đổi đáng kể liên quan đến việc sử dụng kiểu đúc, liên kết và con trỏ được nhập vào cấu trúc. Tuy nhiên mã đường ống thực tế không thay đổi. Chúng ta có thể bỏ qua phiên bản này.

Xv6, một hạt nhân giống Unix đơn giản

Để tạo hạt nhân Xv6 chịu ảnh hưởng của phiên bản thứ sáu của Unix, nhưng được viết bằng ngôn ngữ C hiện đại để chạy trên bộ xử lý x86. Mã này dễ đọc và dễ hiểu. Ngoài ra, không giống như các nguồn Unix có TUHS, bạn có thể biên dịch, sửa đổi và chạy nó trên một thứ khác ngoài PDP 11/70. Do đó, lõi này được sử dụng rộng rãi trong các trường đại học như một tài liệu giảng dạy về hệ điều hành. nguồn đang ở trên Github.

Mã chứa một triển khai rõ ràng và chu đáo đường ống.c, được hỗ trợ bởi bộ đệm trong bộ nhớ thay vì inode trên đĩa. Ở đây tôi chỉ đưa ra định nghĩa về "đường ống kết cấu" và chức năng 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 sysfile.c. Tôi khuyên bạn nên đọc tất cả mã của anh ấy. Độ phức tạp ở mức mã nguồn của phiên bản thứ sáu, nhưng nó dễ đọc và dễ chịu hơn nhiều.

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 "Nhân Linux triển khai đường ống và FIFOlà tổng quan về cách hoạt động của các đường dẫn Linux (cho đến nay). MỘT cam kết gần đây trên linux minh họa mô hình tương tác đường ống, có khả năng vượt quá khả năng của các tệp tạm thời; và cũng cho thấy các đường ống đã đi bao xa từ "khóa rất thận trọng" trong nhân Unix phiên bản thứ sáu.

Nguồn: www.habr.com

Thêm một lời nhận xét