GNU/Linux >> Belajar Linux >  >> Linux

Mengapa forking proses saya menyebabkan file dibaca tanpa batas

Saya terkejut bahwa ada masalah, tetapi tampaknya ada masalah di Linux (saya menguji di Ubuntu 16.04 LTS yang berjalan di VMWare Fusion VM di Mac saya) — tetapi itu bukan masalah di Mac saya yang menjalankan macOS 10.13. 4 (High Sierra), dan saya juga tidak berharap ini menjadi masalah pada varian Unix lainnya.

Seperti yang saya catat dalam komentar:

Ada deskripsi file terbuka dan deskriptor file terbuka di belakang setiap aliran. Ketika proses bercabang, anak memiliki kumpulan deskriptor file terbuka (dan aliran file) sendiri, tetapi setiap deskriptor file pada anak berbagi deskripsi file terbuka dengan induk. JIKA (dan itu 'jika' besar) proses anak menutup deskriptor file terlebih dahulu melakukan yang setara dengan lseek(fd, 0, SEEK_SET) , maka itu juga akan memposisikan deskriptor file untuk proses induk, dan itu dapat menyebabkan loop tak terbatas. Namun, saya belum pernah mendengar tentang perpustakaan yang mencari itu; tidak ada alasan untuk melakukannya.

Lihat POSIX open() dan fork() untuk informasi selengkapnya tentang deskriptor file terbuka dan deskripsi file terbuka.

Deskriptor file terbuka bersifat pribadi untuk suatu proses; deskripsi file terbuka dibagikan oleh semua salinan deskriptor file yang dibuat oleh operasi 'file terbuka' awal. Salah satu properti utama dari deskripsi file terbuka adalah posisi pencarian saat ini. Artinya, proses turunan dapat mengubah posisi pencarian saat ini untuk induk — karena berada dalam deskripsi file terbuka bersama.

neof97.c

Saya menggunakan kode berikut — versi asli yang sedikit diadaptasi yang dikompilasi dengan rapi dengan opsi kompilasi yang ketat:

#include "posixver.h"
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

enum { MAX = 100 };

int main(void)
{
    if (freopen("input.txt", "r", stdin) == 0)
        return 1;
    char s[MAX];
    for (int i = 0; i < 30 && fgets(s, MAX, stdin) != NULL; i++)
    {
        // Commenting out this region fixes the issue
        int status;
        pid_t pid = fork();
        if (pid == 0)
        {
            exit(0);
        }
        else
        {
            waitpid(pid, &status, 0);
        }
        // End region
        printf("%s", s);
    }
    return 0;
}

Salah satu modifikasi membatasi jumlah siklus (anak-anak) menjadi hanya 30. Saya menggunakan file data dengan 4 baris 20 huruf acak ditambah baris baru (total 84 byte):

ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe

Saya menjalankan perintah di bawah strace di Ubuntu:

$ strace -ff -o st-out -- neof97
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
…
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
$

Ada 31 file dengan nama form st-out.808## di mana hash adalah angka 2 digit. File proses utama cukup besar; yang lainnya kecil, dengan salah satu ukuran 66, 110, 111, atau 137:

$ cat st-out.80833
lseek(0, -63, SEEK_CUR)                 = 21
exit_group(0)                           = ?
+++ exited with 0 +++
$ cat st-out.80834
lseek(0, -42, SEEK_CUR)                 = -1 EINVAL (Invalid argument)
exit_group(0)                           = ?
+++ exited with 0 +++
$ cat st-out.80835
lseek(0, -21, SEEK_CUR)                 = 0
exit_group(0)                           = ?
+++ exited with 0 +++
$ cat st-out.80836
exit_group(0)                           = ?
+++ exited with 0 +++
$

Kebetulan 4 anak pertama masing-masing menunjukkan salah satu dari empat perilaku — dan setiap rangkaian 4 anak berikutnya menunjukkan pola yang sama.

Ini menunjukkan bahwa tiga dari empat anak benar-benar melakukan lseek() pada input standar sebelum keluar. Jelas, saya sekarang telah melihat perpustakaan melakukannya. Saya tidak tahu mengapa itu dianggap sebagai ide yang bagus, tetapi secara empiris, itulah yang terjadi.

neof67.c

Versi kode ini, menggunakan aliran file terpisah (dan deskriptor file) dan fopen() bukannya freopen() juga mengalami masalah.

#include "posixver.h"
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

enum { MAX = 100 };

int main(void)
{
    FILE *fp = fopen("input.txt", "r");
    if (fp == 0)
        return 1;
    char s[MAX];
    for (int i = 0; i < 30 && fgets(s, MAX, fp) != NULL; i++)
    {
        // Commenting out this region fixes the issue
        int status;
        pid_t pid = fork();
        if (pid == 0)
        {
            exit(0);
        }
        else
        {
            waitpid(pid, &status, 0);
        }
        // End region
        printf("%s", s);
    }
    return 0;
}

Ini juga menunjukkan perilaku yang sama, kecuali deskriptor file tempat pencarian terjadi adalah 3 bukannya 0 . Jadi, dua hipotesis saya tidak terbukti — terkait dengan freopen() dan stdin; keduanya ditampilkan salah oleh kode pengujian kedua.

Diagnosis awal

IMO, ini bug. Anda seharusnya tidak dapat mengalami masalah ini. Kemungkinan besar bug di perpustakaan Linux (GNU C) daripada kernel. Ini disebabkan oleh lseek() dalam proses anak. Tidak jelas (karena saya belum melihat kode sumbernya) apa yang perpustakaan lakukan atau mengapa.

GLIBC Bug 23151

GLIBC Bug 23151 - Proses bercabang dengan file yang tidak ditutup mencari sebelum keluar dan dapat menyebabkan pengulangan tak terbatas pada I/O induk.

Bug dibuat 08-05-2018 AS/Pasifik, dan ditutup sebagai INVALID pada 09-05-2018. Alasan yang diberikan adalah:

Silakan bacahttp://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_05_01, khususnya paragraf ini:

Perhatikan bahwa setelah fork() , ada dua pegangan di tempat yang sebelumnya ada. […]

POSIX

Bagian lengkap dari POSIX yang dirujuk (selain dari kata-kata yang mencatat bahwa ini tidak dicakup oleh standar C) adalah ini:

2.5.1 Interaksi Deskriptor File dan Aliran I/O Standar

Deskripsi file terbuka dapat diakses melalui deskriptor file, yang dibuat menggunakan fungsi seperti open() atau pipe() , atau melalui aliran, yang dibuat menggunakan fungsi seperti fopen() atau popen() . Baik deskriptor file atau aliran disebut "pegangan" pada deskripsi file terbuka yang dirujuknya; deskripsi file terbuka mungkin memiliki beberapa pegangan.

Pegangan dapat dibuat atau dihancurkan oleh tindakan pengguna eksplisit, tanpa memengaruhi deskripsi file terbuka yang mendasarinya. Beberapa cara untuk membuatnya termasuk fcntl() , dup() , fdopen() , fileno() , dan fork() . Mereka dapat dihancurkan setidaknya dengan fclose() , close() , dan exec fungsi.

Deskriptor file yang tidak pernah digunakan dalam operasi yang dapat memengaruhi offset file (misalnya, read() , write() , atau lseek() ) tidak dianggap sebagai pegangan untuk diskusi ini, tetapi dapat memunculkannya (misalnya, sebagai akibat dari fdopen() , dup() , atau fork() ). Pengecualian ini tidak menyertakan deskriptor file yang mendasari aliran, baik yang dibuat dengan fopen() atau fdopen() , selama tidak digunakan langsung oleh aplikasi untuk mempengaruhi file offset. read() dan write() fungsi secara implisit mempengaruhi offset file; lseek() secara eksplisit memengaruhinya.

Hasil pemanggilan fungsi yang melibatkan salah satu pegangan ("pegangan aktif") didefinisikan di tempat lain dalam volume POSIX.1-2017 ini, tetapi jika dua atau lebih pegangan digunakan, dan salah satunya adalah aliran, aplikasi harus memastikan bahwa tindakan mereka terkoordinasi seperti yang dijelaskan di bawah ini. Jika ini tidak dilakukan, hasilnya tidak ditentukan.

Pegangan yang merupakan aliran dianggap ditutup saat fclose() , atau freopen() dengan nama file yang tidak lengkap, dijalankan di dalamnya (untuk freopen() dengan nama file null, itu ditentukan implementasi apakah pegangan baru dibuat atau yang sudah ada digunakan kembali), atau ketika proses yang memiliki aliran itu berakhir dengan exit() , abort() , atau karena sinyal. Deskriptor file ditutup oleh close() , _exit() , atau exec() berfungsi saat FD_CLOEXEC diatur pada deskriptor file tersebut.

[sic] Menggunakan 'non-full' mungkin salah ketik untuk 'non-null'.

Agar pegangan menjadi pegangan aktif, aplikasi harus memastikan bahwa tindakan di bawah ini dilakukan antara penggunaan terakhir pegangan (pegangan aktif saat ini) dan penggunaan pertama pegangan kedua (pegangan aktif masa depan). Pegangan kedua kemudian menjadi pegangan aktif. Semua aktivitas oleh aplikasi yang mempengaruhi offset file pada pegangan pertama harus ditangguhkan sampai lagi menjadi pegangan file aktif. (Jika fungsi aliran memiliki fungsi dasar yang memengaruhi offset file, fungsi streaming akan dianggap memengaruhi offset file.)

Pegangan tidak harus dalam proses yang sama agar aturan ini dapat diterapkan.

Perhatikan bahwa setelah fork() , ada dua pegangan di tempat yang sebelumnya ada. Aplikasi harus memastikan bahwa, jika kedua pegangan dapat diakses, keduanya dalam keadaan di mana yang lain dapat menjadi pegangan aktif terlebih dahulu. Aplikasi harus mempersiapkan fork() persis seolah-olah itu adalah perubahan pegangan aktif. (Jika satu-satunya tindakan yang dilakukan oleh salah satu proses adalah salah satu dari exec() fungsi atau _exit() (bukan exit() ), pegangan tidak pernah diakses dalam proses itu.)

Untuk pegangan pertama, ketentuan yang berlaku pertama di bawah ini berlaku. Setelah tindakan yang diperlukan di bawah dilakukan, jika pegangan masih terbuka, aplikasi dapat menutupnya.

  • Jika ini adalah deskriptor file, tidak diperlukan tindakan apa pun.

  • Jika satu-satunya tindakan lebih lanjut yang harus dilakukan pada pegangan apa pun untuk deskriptor file terbuka ini adalah menutupnya, tidak ada tindakan yang perlu dilakukan.

  • Jika itu adalah aliran yang tidak di-buffer, tidak ada tindakan yang perlu dilakukan.

  • Jika itu adalah aliran yang buffer baris, dan byte terakhir yang ditulis ke aliran adalah <newline> (yaitu, seolah-olah putc('\n') adalah operasi terbaru pada streaming tersebut), tidak ada tindakan yang perlu diambil.

  • Jika itu adalah aliran yang terbuka untuk menulis atau menambahkan (tetapi tidak juga terbuka untuk membaca), aplikasi harus melakukan fflush() , atau streaming akan ditutup.

  • Jika aliran terbuka untuk dibaca dan berada di akhir file (feof() benar), tidak ada tindakan yang perlu dilakukan.

  • Jika aliran terbuka dengan mode yang memungkinkan membaca dan deskripsi file terbuka yang mendasarinya merujuk ke perangkat yang mampu mencari, aplikasi harus menjalankan fflush() , atau streaming akan ditutup.

Untuk pegangan kedua:

  • Jika ada pegangan aktif sebelumnya yang telah digunakan oleh fungsi yang secara eksplisit mengubah offset file, kecuali seperti yang diperlukan di atas untuk pegangan pertama, aplikasi harus melakukan lseek() atau fseek() (yang sesuai dengan jenis pegangan) ke lokasi yang sesuai.

Jika pegangan aktif berhenti dapat diakses sebelum persyaratan pada pegangan pertama, di atas, telah terpenuhi, status deskripsi file terbuka menjadi tidak terdefinisi. Ini mungkin terjadi selama fungsi seperti fork() atau _exit() .

exec() fungsi membuat tidak dapat diakses semua aliran yang terbuka pada saat dipanggil, terlepas dari aliran atau deskriptor file mana yang mungkin tersedia untuk gambar proses baru.

Ketika peraturan ini diikuti, terlepas dari urutan pegangan yang digunakan, implementasi harus memastikan bahwa aplikasi, bahkan yang terdiri dari beberapa proses, akan menghasilkan hasil yang benar:tidak ada data yang hilang atau digandakan saat menulis, dan semua data harus ditulis dalam memesan, kecuali seperti yang diminta oleh pencari. Ini ditentukan oleh implementasi apakah, dan dalam kondisi apa, semua masukan terlihat tepat satu kali.

Setiap fungsi yang beroperasi pada aliran dikatakan memiliki nol atau lebih "fungsi dasar". Ini berarti bahwa fungsi aliran memiliki ciri-ciri tertentu yang sama dengan fungsi dasarnya, tetapi tidak mengharuskan adanya hubungan apa pun antara penerapan fungsi aliran dan fungsi dasarnya.

Eksegesis

Itu bacaan yang sulit! Jika Anda tidak jelas tentang perbedaan antara deskriptor file terbuka dan deskripsi file terbuka, baca spesifikasi open() dan fork() (dan dup() atau dup2() ). Definisi untuk deskriptor file dan deskripsi file terbuka juga relevan, jika singkat.

Dalam konteks kode dalam pertanyaan ini (dan juga untuk proses anak yang tidak diinginkan yang dibuat saat membaca file), kami memiliki pegangan aliran file yang terbuka hanya untuk membaca yang belum menemukan EOF (jadi feof() tidak akan mengembalikan true, meskipun posisi baca ada di akhir file).

Salah satu bagian penting dari spesifikasi adalah:Aplikasi harus mempersiapkan fork() persis seolah-olah itu adalah perubahan pegangan aktif.

Ini berarti bahwa langkah-langkah yang diuraikan untuk 'pegangan file pertama' relevan, dan melewatinya, ketentuan pertama yang berlaku adalah yang terakhir:

  • Jika aliran terbuka dengan mode yang memungkinkan membaca dan deskripsi file terbuka yang mendasarinya merujuk ke perangkat yang mampu mencari, aplikasi harus menjalankan fflush() , atau streaming akan ditutup.

Jika Anda melihat definisi untuk fflush() , Anda menemukan:

Jika streaming menunjuk ke aliran keluaran atau aliran pembaruan di mana operasi terbaru tidak dimasukkan, fflush() akan menyebabkan data tidak tertulis apa pun untuk aliran tersebut ditulis ke file, [CX] ⌦ dan modifikasi data terakhir serta stempel waktu perubahan status file terakhir dari file yang mendasarinya harus ditandai untuk pembaruan.

Untuk aliran terbuka untuk membaca dengan deskripsi file yang mendasarinya, jika file tersebut belum di EOF, dan file tersebut mampu mencari, offset file dari deskripsi file terbuka yang mendasari harus diatur ke posisi file aliran, dan karakter apa pun didorong kembali ke aliran dengan ungetc() atau ungetwc() yang selanjutnya tidak dibaca dari aliran akan dibuang (tanpa mengubah offset file lebih lanjut). ⌫

Tidak begitu jelas apa yang terjadi jika Anda menerapkan fflush() ke aliran input yang terkait dengan file yang tidak dapat dicari, tetapi itu bukan perhatian langsung kami. Namun, jika Anda menulis kode perpustakaan generik, Anda mungkin perlu mengetahui apakah deskriptor file yang mendasarinya dapat dicari sebelum melakukan fflush() di sungai. Alternatifnya, gunakan fflush(NULL) agar sistem melakukan apa pun yang diperlukan untuk semua aliran I/O, mencatat bahwa ini akan kehilangan semua karakter yang didorong kembali (melalui ungetc() dll).

lseek() operasi yang ditampilkan di strace output tampaknya mengimplementasikan fflush() semantik yang mengaitkan offset file dari deskripsi file terbuka dengan posisi file aliran.

Jadi, untuk kode di pertanyaan ini, sepertinya fflush(stdin) diperlukan sebelum fork() untuk memastikan konsistensi. Tidak melakukan hal itu mengarah pada perilaku yang tidak terdefinisi ('jika ini tidak dilakukan, hasilnya tidak ditentukan') — seperti pengulangan tanpa batas.


Panggilan exit() menutup semua pegangan file yang terbuka. Setelah fork, anak dan orang tua memiliki salinan tumpukan eksekusi yang identik, termasuk penunjuk FileHandle. Saat anak keluar, ia menutup file dan menyetel ulang penunjuk.

  int main(){
        freopen("input.txt", "r", stdin);
        char s[MAX];
        prompt(s);
        int i = 0;
        char* ret = fgets(s, MAX, stdin);
        while (ret != NULL) {
            //Commenting out this region fixes the issue
            int status;
            pid_t pid = fork();   // At this point both processes has a copy of the filehandle
            if (pid == 0) {
                exit(0);          // At this point the child closes the filehandle
            } else {
                waitpid(pid, &status, 0);
            }
            //End region
            printf("%s", s);
            ret = fgets(s, MAX, stdin);
        }
    }

Linux
  1. Apa arti &di akhir perintah linux?

  2. Mengapa net rpc shutdown gagal dengan kredensial yang tepat?

  3. Mengapa membuat gambar memberi saya file, bukan gambar?

  1. Mengapa Skrip Bash Tidak Mengenal Alias?

  2. Mengapa Pengguna Root Membutuhkan Izin Sudo?

  3. File mana di /proc yang dibaca oleh kernel selama proses boot?

  1. Apakah Tail Membaca Seluruh File?

  2. Apa Artinya Dalam Keluaran Dari Ps?

  3. Mengapa File Descriptor Dibuka Dan Hanya Dibaca Sekali?