Baca file normal dengan syscall Linux

Cara terbaik untuk mulai bekerja dengan fungsi ini adalah dengan membaca file normal. Ini adalah cara paling sederhana untuk menggunakan syscall itu, dan karena suatu alasan: ia tidak memiliki kendala sebanyak jenis aliran atau pipa lainnya. Jika Anda memikirkannya, itulah logikanya, ketika Anda membaca output dari aplikasi lain, Anda perlu menyiapkan beberapa output sebelum membacanya dan karenanya Anda perlu menunggu aplikasi ini untuk menulis output ini.

Pertama, perbedaan utama dengan perpustakaan standar: Tidak ada buffering sama sekali. Setiap kali Anda memanggil fungsi baca, Anda akan memanggil Kernel Linux, dan ini akan memakan waktu –‌  hampir instan jika Anda memanggilnya sekali, tetapi dapat memperlambat Anda jika Anda memanggilnya ribuan kali dalam satu detik. Sebagai perbandingan, perpustakaan standar akan menyangga input untuk Anda. Jadi, setiap kali Anda memanggil read, Anda harus membaca lebih dari beberapa byte, melainkan buffer besar seperti beberapa kilobyte –  kecuali jika yang Anda butuhkan benar-benar sedikit byte, misalnya jika Anda memeriksa apakah file ada dan tidak kosong.

Namun ini memiliki manfaat: setiap kali Anda memanggil baca, Anda yakin Anda mendapatkan data yang diperbarui, jika ada aplikasi lain yang memodifikasi file saat ini. Ini sangat berguna untuk file khusus seperti yang ada di /proc atau /sys.

Saatnya menunjukkan kepada Anda dengan contoh nyata. Program C ini memeriksa apakah file tersebut PNG atau tidak. Untuk melakukannya, ia membaca file yang ditentukan di jalur yang Anda berikan dalam argumen command line, dan memeriksa apakah 8 byte pertama sesuai dengan header PNG.

Berikut kodenya:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
 
typedef enum {
IS_PNG,
TOO_SHORT,
INVALID_HEADER
} pngStatus_t;
 
unsigned int isSyscallSuccessful(const ssize_t readStatus) {
return readStatus >= 0;
 
}
 
/*
* checkPngHeader is checking if the pngFileHeader array corresponds to a PNG
* file header.
*
* Currently it only checks the first 8 bytes of the array. If the array is less
* than 8 bytes, TOO_SHORT is returned.
*
* pngFileHeaderLength must cintain the kength of tye array. Any invalid value
* may lead to undefined behavior, such as application crashing.
*
* Returns IS_PNG if it corresponds to a PNG file header. If there's at least
* 8 bytes in the array but it isn't a PNG header, INVALID_HEADER is returned.
*
*/
pngStatus_t checkPngHeader(const unsigned char* const pngFileHeader,
size_t pngFileHeaderLength) { const unsigned char expectedPngHeader[8] =
{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
int i = 0;
 
if (pngFileHeaderLength < sizeof(expectedPngHeader)) {
return TOO_SHORT;
 
}
 
for (i = 0; i < sizeof(expectedPngHeader); i++) {
if (pngFileHeader[i] != expectedPngHeader[i]) {
return INVALID_HEADER;
 
}
}
 
/* If it reaches here, all first 8 bytes conforms to a PNG header. */
return IS_PNG;
}
 
int main(int argumentLength,  char *argumentList[]) {
char *pngFileName = NULL;
unsigned char pngFileHeader[8] = {0};
 
ssize_t readStatus = 0;
/* Linux uses a number to identify a open file. */
int pngFile = 0;
pngStatus_t pngCheckResult;
 
if (argumentLength != 2) {
fputs("You must call this program using isPng {your filename}.n", stderr);
return EXIT_FAILURE;
 
}
 
pngFileName = argumentList[1];
pngFile = open(pngFileName, O_RDONLY);
 
if (pngFile == -1) {
perror("Opening the provided file failed");
return EXIT_FAILURE;
 
}
 
/* Read few bytes to identify if the file is PNG. */
readStatus = read(pngFile, pngFileHeader, sizeof(pngFileHeader));
 
if (isSyscallSuccessful(readStatus)) {
/* Check if the file is a PNG since it got the data. */
pngCheckResult = checkPngHeader(pngFileHeader, readStatus);
 
if        (pngCheckResult == TOO_SHORT) {
printf("file %s isn't a PNG file: it's too short.n", pngFileName);
 
} else if (pngCheckResult == IS_PNG) {
printf("file %s is a PNG file!n", pngFileName);
 
} else {
printf("file %s is not in PNG format.n", pngFileName);
 
}
 
} else {
perror("Reading the file failed");
return EXIT_FAILURE;
 
}
 
/* Close the file... */
if (close(pngFile) == -1) {
perror("Closing the provided file failed");
return EXIT_FAILURE;
 
}
 
pngFile = 0;
 
return EXIT_SUCCESS;
 
}

Lihat, ini adalah contoh yang lengkap, berfungsi, dan dapat dikompilasi. Jangan ragu untuk mengkompilasi sendiri dan mengujinya, itu benar-benar berfungsi. Anda harus memanggil program dari terminal seperti ini:

./isPng {your filename}

Sekarang, mari kita fokus pada panggilan baca itu sendiri:

pngFile = open(pngFileName, O_RDONLY);

if (pngFile == -1) {
perror("Opening the provided file failed");
return EXIT_FAILURE;

}

/* Read few bytes to identify if the file is PNG. */
readStatus = read(pngFile, pngFileHeader, sizeof(pngFileHeader));

Tanda tangan baca adalah sebagai berikut (diekstrak dari halaman manual Linux):

ssize_t read(int fd, void *buf, size_t count);

Pertama, argumen fd mewakili deskriptor file. Saya telah menjelaskan sedikit konsep ini di artikel fork saya
. Deskriptor file adalah int yang mewakili file terbuka, soket, pipa, FIFO, perangkat, nah itu banyak hal di mana data dapat dibaca atau ditulis, umumnya dengan cara seperti aliran. Saya akan membahas lebih dalam tentang itu di artikel mendatang.

fungsi buka adalah salah satu cara untuk memberitahu ke Linux: Saya ingin melakukan sesuatu dengan file di jalur itu, tolong temukan di mana itu dan beri saya akses ke sana. Ini akan memberi Anda kembali int yang disebut deskriptor file dan sekarang, jika Anda ingin melakukan sesuatu dengan file ini, gunakan nomor itu. Jangan lupa untuk memanggil tutup setelah Anda selesai dengan file, seperti pada contoh.

Jadi Anda perlu memberikan nomor khusus ini untuk dibaca. Lalu ada argumen buff. Di sini Anda harus memberikan pointer ke array tempat read akan menyimpan data Anda. Akhirnya, count adalah berapa banyak byte yang akan dibaca paling banyak.

Nilai yang dikembalikan adalah tipe ssize_t. Tipe yang aneh bukan? Itu berarti “ukuran_t yang ditandatangani”, pada dasarnya ini adalah int yang panjang. Ini mengembalikan jumlah byte yang berhasil dibaca, atau -1 jika ada masalah. Anda dapat menemukan penyebab pasti masalah dalam variabel global errno yang dibuat oleh Linux, yang didefinisikan dalam <errno.h>. Tetapi untuk mencetak pesan error, menggunakan perror lebih baik karena mencetak errno atas nama Anda.

Dalam file normal – dan onlydalam hal ini – read akan mengembalikan kurang dari hitungan hanya jika Anda telah mencapai akhir file. Array buf yang Anda berikanmust cukup besar untuk memuat setidaknya hitungan byte, atau program Anda mungkin macet atau membuat bug keamanan.

Sekarang, read tidak hanya berguna untuk file biasa dan jika Anda ingin merasakan kekuatan supernya –  Ya, saya tahu itu tidak ada dalam komik Marvel mana pun tetapi memiliki kekuatan sebenarnya  – Anda akan ingin menggunakannya dengan aliran lain seperti pipa atau soket. Mari kita lihat itu:

File khusus Linux dan baca panggilan sistem

Fakta membaca bekerja dengan berbagai file seperti pipa, soket, FIFO atau perangkat khusus seperti disk atau port serial adalah apa yang membuatnya benar-benar lebih kuat. Dengan beberapa adaptasi, Anda dapat melakukan hal-hal yang sangat menarik. Pertama, ini berarti Anda benar-benar dapat menulis fungsi yang bekerja pada file dan menggunakannya dengan pipa sebagai gantinya. Itu menarik untuk melewatkan data tanpa pernah memukul disk, memastikan kinerja terbaik.

Namun ini memicu aturan khusus juga. Mari kita ambil contoh pembacaan baris dari terminal dibandingkan dengan file normal. Saat Anda memanggil read pada file normal, hanya perlu beberapa milidetik ke Linux untuk mendapatkan jumlah data yang Anda minta.

Tetapi ketika datang ke terminal, itu cerita lain: katakanlah Anda meminta nama user. Pengguna mengetik di terminal nama user dan tekan Enter. Sekarang Anda mengikuti saran saya di atas dan Anda memanggil read dengan buffer besar seperti 256 byte.

Jika read berfungsi seperti halnya dengan file, itu akan menunggu user mengetik 256 karakter sebelum kembali! Pengguna Anda akan menunggu selamanya, dan kemudian dengan sedih mematikan aplikasi Anda. Ini tentu bukan yang Anda inginkan, dan Anda akan mendapat masalah besar.

Oke, Anda bisa membaca satu byte pada satu waktu tetapi solusi ini sangat tidak efisien, seperti yang saya katakan di atas. Itu harus bekerja lebih baik dari itu.

Tetapi pengembang Linux berpikir membaca secara berbeda untuk menghindari masalah ini:

  • Ketika Anda membaca file normal, ia mencoba sebanyak mungkin untuk membaca jumlah byte dan secara aktif akan mendapatkan byte dari disk jika diperlukan.
  • Untuk semua jenis file lainnya, itu akan kembali as soon as ada beberapa data yang tersedia dan at most menghitung byte:
    1. Untuk terminal, biasanya ketika user menekan tombol Enter.
    2. Untuk soket TCP, segera setelah komputer Anda menerima sesuatu, tidak peduli jumlah byte yang didapatnya.
    3. Untuk FIFO atau pipa, umumnya jumlah yang sama seperti yang ditulis oleh aplikasi lain, tetapi kernel Linux dapat mengirimkan lebih sedikit pada satu waktu jika itu lebih nyaman.

Jadi Anda dapat menelepon dengan aman menggunakan buffer 2 KiB tanpa terkunci selamanya. Catatan itu juga bisa terganggu jika aplikasi menerima sinyal. Karena membaca dari semua sumber ini dapat memakan waktu beberapa detik atau bahkan berjam-jam –&nbsp
; sampai pihak lain memutuskan untuk menulis, bagaimanapun juga  – terganggu oleh sinyal memungkinkan untuk berhenti diblokir terlalu lama.

Ini juga memiliki kelemahan: ketika Anda ingin membaca 2 KiB secara tepat dengan file khusus ini, Anda harus memeriksa nilai kembalian read dan call read beberapa kali. read jarang akan mengisi seluruh buffer Anda. Jika aplikasi Anda menggunakan sinyal, Anda juga perlu memeriksa apakah pembacaan gagal dengan -1 karena terputus oleh sinyal, menggunakan errno.

Mari saya tunjukkan bagaimana menarik untuk menggunakan properti khusus dari read:

#define _POSIX_C_SOURCE 1 /* sigaction is not available without this #define. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
/*
* isSignal tells if read syscall has been interrupted by a signal.
*
* Returns TRUE if the read syscall has been interrupted by a signal.
*
* Global variables: it reads errno defined in errno.h
*/
unsigned int isSignal(const ssize_t readStatus) {
return (readStatus == -1 && errno == EINTR);
}
unsigned int isSyscallSuccessful(const ssize_t readStatus) {
return readStatus >= 0;
}
/*
* shouldRestartRead tells when the read syscall has been interrupted by a
* signal event or not, and given this "error" reason is transient, we can
* safely restart the read call.
*
* Currently, it only checks if read has been interrupted by a signal, but it
* could be improved to check if the target number of bytes was read and if it's
* not the case, return TRUE to read again.
*
*/
unsigned int shouldRestartRead(const ssize_t readStatus) {
return isSignal(readStatus);

}

/*
* We need an empty handler as the read syscall will be interrupted only if the
* signal is handled.
*/
void emptyHandler(int ignored) {
return;

}

int main() {
/* Is in seconds. */
const int alarmInterval = 5;
const struct sigaction emptySigaction = {emptyHandler};
char lineBuf[256] = {0};
ssize_t readStatus = 0;
unsigned int waitTime = 0;

/* Do not modify sigaction except if you exactly know what you're doing. */
sigaction(SIGALRM, &emptySigaction, NULL);
alarm(alarmInterval);

fputs("Your text:n", stderr);

do {
/* Don't forget the ' ' */
readStatus = read(STDIN_FILENO, lineBuf, sizeof(lineBuf) - 1);

if (isSignal(readStatus)) {
waitTime += alarmInterval;
alarm(alarmInterval);

fprintf(stderr, "%u secs of inactivity...n", waitTime);

}

} while (shouldRestartRead(readStatus));

if (isSyscallSuccessful(readStatus)) {
/* Terminate the string to avoid a bug when providing it to fprintf. */
lineBuf[readStatus] = ' ';
fprintf(stderr, "You typed %lu chars. Here's your string:n%sn", strlen(lineBuf),
 lineBuf);

} else {
perror("Reading from stdin failed");
return EXIT_FAILURE;

}

return EXIT_SUCCESS;

}

Sekali lagi, ini adalah aplikasi C lengkap yang dapat Anda kompilasi dan jalankan.

Ia melakukan hal berikut: ia membaca baris dari input standar. Namun, setiap 5 detik, ia mencetak baris yang memberi tahu user bahwa belum ada input yang diberikan.

Contoh jika saya menunggu 23 detik sebelum mengetik “Penguin”:

$ alarm_read
Your text:
5 secs of inactivity...
10 secs of inactivity...
15 secs of inactivity...
20 secs of inactivity...
Penguin
You typed 8 chars. Here's your string:
Penguin

Itu sangat berguna. Ini dapat digunakan untuk sering memperbarui UI untuk mencetak kemajuan pembacaan atau pemrosesan aplikasi yang Anda lakukan. Ini juga dapat digunakan sebagai mekanisme batas waktu. Anda juga bisa terganggu oleh sinyal lain yang mungkin berguna untuk aplikasi Anda. Bagaimanapun, ini berarti aplikasi Anda sekarang bisa responsif alih-alih macet selamanya.

Jadi manfaatnya lebih besar daripada kerugian yang dijelaskan di atas. Jika Anda bertanya-tanya apakah Anda harus mendukung file khusus dalam aplikasi yang biasanya bekerja dengan file normal –  dan memanggil read in a loop  – saya akan mengatakan melakukannya kecuali jika Anda sedang terburu-buru, pengalaman pribadi saya sering membuktikan bahwa mengganti file dengan pipa atau FIFO benar-benar dapat membuat aplikasi jauh lebih berguna dengan upaya kecil. Bahkan ada fungsi C yang dibuat sebelumnya di Internet yang mengimplementasikan loop itu untuk Anda: itu disebut fungsi readn.

Kesimpulan

Seperti yang Anda lihat, fread dan read mungkin terlihat mirip, padahal tidak. Dan dengan hanya sedikit perubahan pada cara kerja read untuk pengembang C, read jauh lebih menarik untuk merancang solusi baru untuk masalah yang Anda temui selama pengembangan aplikasi.

Lain kali, saya akan memberi tahu Anda bagaimana menulis syscall bekerja, karena membaca itu keren, tetapi bisa melakukan keduanya jauh lebih baik. Sementara itu, bereksperimenlah dengan membaca, kenali, dan saya ucapkan Selamat Tahun Baru!

Related Posts