Tại sao mèo x >> x lặp?


17

Các lệnh bash sau đi vào một vòng lặp infinte:

$ echo hi > x
$ cat x >> x

Tôi có thể đoán rằng cattiếp tục đọc từ xsau khi nó đã bắt đầu viết lên thiết bị xuất chuẩn. Tuy nhiên, điều khó hiểu là việc thực hiện thử nghiệm con mèo của riêng tôi thể hiện hành vi khác nhau:

// mycat.c
#include <stdio.h>

int main(int argc, char **argv) {
  FILE *f = fopen(argv[1], "rb");
  char buf[4096];
  int num_read;
  while ((num_read = fread(buf, 1, 4096, f))) {
    fwrite(buf, 1, num_read, stdout);
    fflush(stdout);
  }

  return 0;
}

Nếu tôi chạy:

$ make mycat
$ echo hi > x
$ ./mycat x >> x

không lặp. Với hành vi catvà thực tế là stdouttrước đây tôi freadđược gọi lại, tôi sẽ mong đợi mã C này tiếp tục đọc và viết trong một chu kỳ.

Làm thế nào là hai hành vi phù hợp? Cơ chế nào giải thích tại sao catcác vòng lặp trong khi đoạn mã trên không có?


Nó không lặp cho tôi. Bạn đã thử chạy nó dưới strace / giàn? Bạn đang trên hệ thống nào?
Stéphane Chazelas

Có vẻ như mèo BSD có hành vi này và mèo GNU báo lỗi khi chúng tôi thử một cái gì đó như thế này. Câu trả lời này thảo luận tương tự và tôi tin rằng bạn đang sử dụng mèo BSD vì tôi có mèo GNU và khi được kiểm tra đã gặp lỗi.
Ramesh

Tôi đang sử dụng Darwin. Tôi thích ý tưởng cat x >> xgây ra lỗi; tuy nhiên, lệnh này được đề xuất trong cuốn sách Unix của Kernighan và Pike như một bài tập.
Tyler

3
catnhiều khả năng sử dụng các cuộc gọi hệ thống thay vì stdio. Với stdio, chương trình của bạn có thể được lưu trữ EOFness. Nếu bạn bắt đầu với một tệp lớn hơn 4096 byte, bạn có nhận được một vòng lặp vô hạn không?
Đánh dấu Plotnick

@MarkPlotnick, vâng! Các mã C lặp khi tệp lớn hơn 4k. Cảm ơn, có lẽ đó là toàn bộ sự khác biệt ngay tại đó.
Tyler

Câu trả lời:


12

Trên một hệ thống RHEL cũ tôi đã có, /bin/catkhông không vòng lặp cho cat x >> x. catđưa ra thông báo lỗi "cat: x: tệp đầu vào là tệp đầu ra". Tôi có thể đánh lừa /bin/catbằng cách này : cat < x >> x. Khi tôi thử mã của bạn ở trên, tôi nhận được "vòng lặp" mà bạn mô tả. Tôi cũng đã viết một cuộc gọi hệ thống dựa trên "con mèo":

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int
main(int ac, char **av)
{
        char buf[4906];
        int fd, cc;
        fd = open(av[1], O_RDONLY);
        while ((cc = read(fd, buf, sizeof(buf))) > 0)
                if (cc > 0) write(1, buf, cc);
        close(fd);
        return 0;
}

Vòng lặp này cũng vậy. Bộ đệm duy nhất ở đây (không giống như "mycat" dựa trên stdio) là những gì diễn ra trong kernel.

Tôi nghĩ những gì đang xảy ra là bộ mô tả tệp 3 (kết quả của open(av[1])) có phần bù vào tệp 0. Bộ mô tả được ghi 1 (stdout) có độ lệch là 3, vì ">>" khiến trình vỏ gọi thực hiện lseek()trên mô tả tập tin trước khi đưa nó cho catquá trình con.

Thực hiện read()bất kỳ loại nào, cho dù vào bộ đệm stdio, hoặc đơn giản char buf[]nâng cao vị trí của bộ mô tả tệp 3. Thực hiện write()nâng cao vị trí của bộ mô tả tệp 1. Hai độ lệch này là các số khác nhau. Do ">>", bộ mô tả tệp 1 luôn có độ lệch lớn hơn hoặc bằng độ lệch của bộ mô tả tệp 3. Vì vậy, bất kỳ chương trình "giống như con mèo" nào cũng sẽ lặp lại, trừ khi nó thực hiện một số bộ đệm nội bộ. Có thể, thậm chí có khả năng, đó là một triển khai stdio của một FILE *(đó là loại ký hiệu stdoutftrong mã của bạn) bao gồm bộ đệm của chính nó. fread()thực sự có thể thực hiện một cuộc gọi hệ thống read()để điền vào bộ đệm nội bộ cho f. Điều này có thể hoặc không thể thay đổi bất cứ điều gì trong phần bên trong của stdout. Gọi fwrite()vàostdoutcó thể hoặc không thể thay đổi bất cứ điều gì bên trong f. Vì vậy, một "con mèo" dựa trên stdio có thể không lặp. Hoặc nó có thể. Khó có thể nói mà không đọc qua rất nhiều mã libc xấu xí, xấu xí.

Tôi đã thực hiện stracetrên RHEL cat- nó chỉ thực hiện một loạt các cuộc gọi read()write()hệ thống. Nhưng catkhông phải làm việc theo cách này. Nó sẽ có thể vào mmap()các tập tin đầu vào, sau đó làm write(1, mapped_address, input_file_size). Nhân sẽ làm tất cả công việc. Hoặc bạn có thể thực hiện một sendfile()cuộc gọi hệ thống giữa các mô tả tệp đầu vào và đầu ra trên các hệ thống Linux. Các hệ thống SunOS 4.x cũ đã được đồn đại để thực hiện thủ thuật lập bản đồ bộ nhớ, nhưng tôi không biết có ai đã từng làm một con mèo dựa trên sendfile chưa. Trong cả hai trường hợp, "vòng lặp" sẽ không xảy ra, vì cả hai write()sendfile()yêu cầu tham số độ dài để chuyển.


Cảm ơn. Trên Darwin, có vẻ như freadcuộc gọi được lưu trong cờ EOF như Mark Plotnick đã đề xuất. Bằng chứng: [1] Mèo Darwin sử dụng đọc, không nhăn mặt; và [2] Cuộc gọi đầu tiên của Darwin gọi __srefill, fp->_flags |= __SEOF;trong một số trường hợp. [1] src.gnu-darwin.org/src/bin/cat/cat.c [2] opensource.apple.com/source/Libc/Libc-167/stdio.subproj/...
Tyler

1
Điều này thật tuyệt vời - tôi là người đầu tiên nâng cấp nó ngày hôm qua. Nó có thể được đáng nói đến là chỉ POSIX xác định chuyển đổi cho catcat -u- u cho unbuffered .
mikeerv

Trên thực tế, >>nên được thực hiện bằng cách gọi open () bằng O_APPENDcờ, điều này khiến mọi thao tác ghi ghi (nguyên tử) ghi vào phần cuối hiện tại của tệp bất kể vị trí của bộ mô tả tệp là gì trước khi đọc. Hành vi này là cần thiết foo >> logfile & bar >> logfileđể hoạt động chính xác, ví dụ - bạn không đủ khả năng để giả định rằng vị trí sau khi kết thúc lần ghi cuối cùng của bạn vẫn là kết thúc của tệp.
Henning Makholm

1

Một triển khai mèo hiện đại (sunos-4.0 1988) sử dụng mmap () để ánh xạ toàn bộ tệp và sau đó gọi 1x write () cho không gian này. Việc triển khai như vậy sẽ không lặp miễn là bộ nhớ ảo cho phép ánh xạ toàn bộ tệp.

Đối với các triển khai khác, nó phụ thuộc vào việc tệp có lớn hơn bộ đệm I / O hay không.


Nhiều cattriển khai không đệm đầu ra của họ ( -ungụ ý). Những người sẽ luôn luôn lặp đi lặp lại.
Stéphane Chazelas

Solaris 11 (SunOS-5.11) dường như không sử dụng mmap () cho các tệp nhỏ (dường như chỉ dùng đến tệp 32769 byte lớn hơn hoặc cao hơn).
Stéphane Chazelas

Đúng -u thường là mặc định. Điều này không ngụ ý một vòng lặp vì việc triển khai có thể đọc toàn bộ kích thước tệp và chỉ thực hiện một lần ghi với buf đó.
schily

Mèo Solaris chỉ vòng lặp nếu kích thước tập tin> tối đa hóa bản đồ hoặc nếu tập tin ban đầu là! = 0.
schily

Những gì tôi quan sát được với Solaris 11. Nó thực hiện một vòng lặp read () nếu độ lệch ban đầu là! = 0 hoặc nếu các tập tin được chia thành 0 và 32768. Trên đó, nó có các vùng lớn của tệp 8MiB tại một thời điểm và không bao giờ dường như hoàn nguyên các vòng lặp read () ngay cả đối với các tệp PiB (được thử nghiệm trên các tệp thưa thớt).
Stéphane Chazelas

0

Như được viết trong cạm bẫy Bash , bạn không thể đọc từ một tệp và ghi vào tệp đó trong cùng một đường dẫn.

Tùy thuộc vào đường ống của bạn làm gì, tệp có thể bị ghi đè (đến 0 byte hoặc có thể với một số byte bằng kích thước của bộ đệm đường ống của hệ điều hành của bạn) hoặc có thể phát triển cho đến khi lấp đầy không gian đĩa có sẵn hoặc đạt tới Giới hạn kích thước tệp của hệ điều hành hoặc hạn ngạch của bạn, v.v.

Giải pháp là sử dụng trình soạn thảo văn bản hoặc biến tạm thời.


-1

Bạn có một số loại điều kiện cuộc đua giữa cả hai x. Một số triển khai cat(ví dụ coreutils 8.23) cấm rằng:

$ cat x >> x
cat: x: input file is output file

Nếu điều này không được phát hiện, hành vi rõ ràng sẽ phụ thuộc vào việc thực hiện (kích thước bộ đệm, v.v.).

Trong mã của bạn, bạn có thể thử thêm một dấu clearerr(f);sau fflush, trong trường hợp tiếp theo freadsẽ trả về lỗi nếu chỉ báo cuối tập tin được đặt.


Có vẻ như một hệ điều hành tốt sẽ có hành vi xác định cho một quy trình với một luồng duy nhất chạy cùng các lệnh đọc / ghi. Trong mọi trường hợp, hành vi mang tính quyết định đối với tôi và tôi chủ yếu hỏi về sự khác biệt.
Tyler

@Tyler IMHO, không có thông số kỹ thuật rõ ràng trong trường hợp này, lệnh trên không có ý nghĩa gì và tính xác định không thực sự quan trọng (ngoại trừ một lỗi như ở đây, đó là hành vi tốt nhất). Đây là một chút giống như i = i++;hành vi không xác định của C , do đó sự khác biệt.
vinc17

1
Không, không có điều kiện chủng tộc ở đây, hành vi được xác định rõ. Tuy nhiên, nó được xác định theo triển khai, tùy thuộc vào kích thước tương đối của tệp và bộ đệm được sử dụng bởi cat.
Gilles 'SO- ngừng trở nên xấu xa'

@Gilles Bạn thấy hành vi đó được xác định rõ / xác định thực hiện ở đâu? Bạn có thể cho một số tài liệu tham khảo? Đặc tả mèo POSIX chỉ nói: "Nó được xác định theo triển khai cho dù bộ đệm tiện ích con mèo xuất ra nếu tùy chọn -u không được chỉ định." Tuy nhiên, khi sử dụng bộ đệm, việc triển khai không phải xác định cách sử dụng bộ đệm; nó có thể không xác định, ví dụ với bộ đệm được xả vào thời gian ngẫu nhiên.
vinc17

@ vinc17 Vui lòng chèn vào thực tế, trong phần bình luận trước đây của tôi. Vâng, về mặt lý thuyết là có thể và tuân thủ POSIX, nhưng không ai làm điều đó.
Gilles 'SO- ngừng trở nên xấu xa'
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.