Panduan Pemrograman Sistem Lintas Platform untuk UNIX dan Windows (Level 1)
Dalam tutorial ini kita akan belajar cara menulis kode lintas platform dalam C menggunakan fungsi sistem pada OS populer (Windows, Linux / Android, macOS &; FreeBSD): fungsi manajemen file dan file I / O, konsol I / O, pipa (tidak disebutkan namanya), eksekusi proses baru. Kita akan menulis fungsi pembantu kecil kita sendiri di atas API sistem userspace tingkat rendah dan menggunakannya sehingga kode utama kita dapat berjalan di OS apa pun tanpa modifikasi. Tutorial ini adalah Level 1, yang termudah. Saya membagi hal-hal sulit menjadi beberapa bagian sehingga sampel kode di sini seharusnya tidak berlebihan bagi mereka yang baru saja mulai memprogram di C. Kita akan membahas perbedaan antara API sistem dan cara membuat antarmuka pemrograman lintas platform yang menyembunyikan semua perbedaan itu dari pengguna.
Saya telah menulis perangkat lunak lintas platform di C untuk waktu yang lama, dan saya ingin berbagi pengalaman saya dengan yang lain. Saya melakukannya dengan harapan tutorial ini akan berguna, membantu Anda mempelajari pemrograman sistem atau mungkin mem-port aplikasi yang ada dari satu OS ke OS lainnya.
Isi:
Intro
Masalah Utama Pemrograman untuk Multiple OS
Tentang contoh
Alokasi Memori
Deteksi Waktu Kompiler
I/O Standar
Pengkodean &; Konversi Data
File I / O: Program gema file sederhana
Kesalahan Sistem
Manajemen File
Properti File
Daftar Direktori
Pipa Tanpa Nama
Menjalankan Program Lain
Menjalankan Program Lain dan Membaca Outputnya
Mendapatkan Tanggal/Waktu Saat Ini
Menangguhkan Pelaksanaan Program
Intro
Salah satu hal yang paling merepotkan dalam pemrograman C adalah persyaratan untuk mendukung multiple OS karena setiap OS memiliki API sistem yang berbeda. Misalnya, jika kita ingin aplikasi kita berjalan di Linux dan Windows, maka kita perlu menulis 2 program berbeda di C. Untuk mengatasi masalah ini kita dapat:
Beralih ke bahasa lain (Go, Python, Java, dll.) yang memberi kita (hampir) pustaka sistem lintas platform yang lengkap. Meski begitu, itu bukan solusi yang tepat untuk semua skenario yang mungkin. Bagaimana jika kita ingin menulis perangkat lunak server berkinerja tinggi seperti nginx? Kami benar-benar membutuhkan C. Bagaimana jika kita perlu membangun logika program kita di sekitar beberapa pustaka C tingkat rendah? Meskipun kita dapat menulis binding perpustakaan yang diperlukan untuk bahasa lain sendiri, tetapi sebaliknya kita hanya dapat menggunakan C. Bagaimana jika kita ingin aplikasi kita berjalan pada sistem tertanam dengan sumber daya perangkat keras terbatas (CPU, memori)? Sekali lagi, kita membutuhkan C.
Gunakan cabang preprocessor dalam kode kita sehingga compiler menggunakan logika yang sesuai untuk setiap OS. Masalah utama dengan pendekatan ini adalah kodenya terlihat sedikit jelek. Ketika semua fungsi dalam kode kita memiliki beberapa cabang di dalamnya, kode menjadi terlalu sulit untuk dibaca dan dipelihara. Dan kemungkinan besar setiap modifikasi dapat merusak sesuatu yang lain di tempat yang paling tidak kita harapkan. Ya, kadang-kadang cabang preprocessor adalah penyelamat mutlak, tetapi kita tidak boleh menggunakannya secara berlebihan - ini adalah salah satu hal di mana kita perlu menjaga keseimbangan.
#if
#ifdef
Gunakan pustaka yang menyembunyikan dari kami perbedaan mendasar antara API sistem. Dengan kata lain, kami menggunakan perpustakaan yang memberi kami satu antarmuka lintas platform yang mudah digunakan. Dan kode pengguna, yang dibangun di atas perpustakaan ini, berfungsi pada beberapa OS. Ini adalah topik tutorial ini.
Masalah Utama Pemrograman untuk Multiple OS
Hal pertama yang harus kita bahas di sini adalah bagaimana API sistem pada OS yang berbeda sebenarnya berbeda dan masalah apa yang harus kita selesaikan ketika menulis kode untuk beberapa OS.
Yang paling penting, Linux, macOS dan FreeBSD - semuanya adalah sistem UNIX. Dalam kebanyakan kasus mereka menyediakan API serupa (yaitu POSIX) yang sangat mengurangi waktu yang dibutuhkan untuk port kode C di antara mereka. Sayangnya, terkadang fungsi sistem dengan nama yang sama (misalnya) memiliki parameter yang berbeda. Terkadang bendera yang kita berikan ke fungsi memiliki perilaku yang berbeda (mis. untuk soket). Terkadang, kode yang ditulis untuk Linux tidak dapat dengan mudah diporting ke OS lain hanya karena Linux memiliki banyak syscall khusus Linux yang tidak tersedia di macOS (mis. ). Kita harus sangat berhati-hati saat menggunakan fungsi sistem secara langsung dalam kode kita. Juga, mengingat semua hal ini sulit, jadi selalu baik untuk meninggalkan komentar di suatu tempat di kode Anda untuk membantu Anda mengingatnya dengan cepat beberapa waktu kemudian. Dengan demikian, kita membutuhkan lapisan tipis antara kode aplikasi kita dan API sistem. Perpustakaan lintas platform adalah lapisan ini yang akan mengatasi masalah yang baru saja saya jelaskan. Namun, sementara menyembunyikan dari kami rincian implementasi untuk setiap OS, perpustakaan yang baik juga harus menjelaskan perbedaan-perbedaan dalam dokumentasinya agar Anda memahami bagaimana tepatnya itu akan beroperasi pada OS tertentu. Jika tidak, kita mungkin berakhir dengan kode yang berkinerja buruk atau tidak terduga pada beberapa sistem.
sendfile()
O_NONBLOCK
sem_timedwait()
Melanjutkan masalah kompatibilitas API di atas, misalkan aplikasi kita sudah menggunakan fungsi khusus Linux, tetapi kita juga ingin menjalankannya di macOS. Kita sekarang berada pada titik ketika kita harus memutuskan: 1) haruskah kita menulis fungsi serupa secara manual untuk macOS atau 2) haruskah kita memikirkan kembali pendekatan kita pada tingkat yang lebih tinggi. Pergi dengan # 1 itu bagus, tetapi kita harus berhati-hati di sini: misalnya, jika kita mencoba mengimplementasikan sendiri untuk macOS, kita mungkin akan menggunakannya untuk meniru itu, tetapi kemudian kita harus yakin bahwa segala sesuatu yang lain (termasuk penanganan sinyal UNIX) bekerja mirip dengan implementasi Linux. Dan meski begitu, bagaimana dengan semaphore bernama, akankah fungsi kita mendukungnya? Hal ini selalu sangat sulit untuk dipertahankan ... Saya pikir kadang-kadang lebih baik mendesain ulang aplikasi dan pergi dengan beberapa solusi alternatif, jika memungkinkan.
sem_timedwait()
pthread_cond_timedwait()
Sekarang mari kita bicara tentang Windows. Tentu saja, Windows bukan UNIX, API-nya benar-benar berbeda di hampir semua aspek, termasuk (tetapi tidak terbatas pada) file, soket, timer, proses, dll. Meskipun Microsoft menyediakan fungsi (misalnya) yang mirip dengan POSIX melalui pustaka runtime C, perilaku mereka mungkin tidak sepenuhnya sama dengan UNIX. Perlu diketahui bahwa Anda mungkin berakhir dengan beberapa masalah tak terduga kecuali Anda membaca dokumentasi 100% dari Microsoft Docs dan memahami dengan tepat bagaimana fungsi tersebut bekerja di dalamnya. Secara teori, seharusnya hanya pembungkus yang sangat tipis, tetapi saya tidak akan yakin itu kecuali saya melihat kodenya. Tetapi mengapa mencoba mempelajari cara menggunakan semua fungsi pembungkus ini dengan benar, ketika kita sudah memiliki dokumentasi yang dijelaskan dengan sangat baik dan tepat untuk semua fungsi WinAPI tingkat rendah (mis.)? Itu sebabnya untuk pekerjaan saya, saya selalu mencoba menggunakan fungsi WinAPI secara langsung, jika memungkinkan, dan bukan beberapa pembungkus di sekitarnya.
_open()
_open()
CreateFileW()
CreateFileW()
Sistem UNIX menggunakan karakter untuk jalur file, tetapi Windows biasanya menggunakan file . Namun, sebagian besar fungsi WinAPI juga menerima jalur dan berfungsi dengan benar. Jadi kita dapat mengatakan bahwa Windows mendukung keduanya dan sebagai karakter pembagi jalur, tetapi ingatlah bahwa mungkin tidak berfungsi dalam beberapa kasus yang jarang terjadi.
/
\
/
\
/
/
Ada kemungkinan konflik nama saat mengkompilasi kode untuk platform yang berbeda. Terkadang kode Anda yang benar tidak dikompilasi di OS lain karena kesalahan kompilasi yang sangat aneh, yang pada awalnya cukup sulit dipahami. Saat itulah Anda menggunakan beberapa variabel atau nama fungsi dalam kode Anda tetapi nama yang sama sudah digunakan di salah satu file header sistem Anda . Masalahnya semakin sulit jika deklarasi ini adalah definisi preprocessor - dalam hal ini kompiler bisa menjadi gila dan pesan kesalahannya tidak banyak membantu. Untuk mencegah masalah ini terjadi, saya sarankan Anda untuk selalu menggunakan awalan yang unik untuk proyek Anda. Beberapa waktu lalu saya mulai menggunakan awalan untuk semua nama dalam kode perpustakaan saya, dan saya tidak ingat apakah saya memiliki konflik nama tunggal sejak saat itu. nginx, misalnya, menggunakan awalan di mana-mana, jadi ini adalah praktik umum untuk proyek C. Perhatikan juga, bahwa ruang nama C ++ tidak banyak membantu dengan masalah di atas, karena Anda masih tidak dapat menggunakan apa yang sudah -d dalam file header sistem - Anda tetap harus terlebih dahulu.
#include
ff
ngx_
#define
#undef
Perlu dikatakan bahwa jika Anda mengkompilasi kode Anda untuk Windows dengan MinGW, ingatlah bahwa file include MinGW tidak identik dengan yang dibundel dengan Microsoft Visual Studio. Perlu diingat bahwa mungkin ada konflik tambahan di sekitar nama global - itu tergantung pada apa yang termasuk file yang digunakan.
Perbedaan lain antara fungsi sistem Windows dan UNIX adalah pengkodean teks. Ketika saya ingin membuka file dengan nama yang mengandung karakter non-latin, saya perlu menggunakan pengkodean teks yang benar, jika tidak, sistem tidak akan memahami saya dan membuka file yang salah atau kembali dengan kesalahan. Secara default, sistem UNIX biasanya menggunakan pengkodean UTF-8, sedangkan Windows menggunakan UTF-16LE. Dan perbedaan ini saja menghentikan kita untuk menggunakan fungsi API dengan mudah langsung dari kode kita. Jika kita mencobanya, kita akan berakhir dengan banyak di dalam fungsi kita. Jadi perpustakaan kita tidak hanya harus menangani nama dan parameter fungsi API sistem, tetapi juga secara otomatis mengonversi teks ke pengkodean yang benar untuk mereka. Saya menggunakan UTF-8 untuk proyek saya, dan saya merekomendasikan semua orang melakukannya. UTF-16LE tidak nyaman dalam banyak aspek, termasuk fakta bahwa ini adalah pilihan yang jauh kurang populer untuk dokumen teks yang mungkin Anda temukan di Internet. UTF-8 hampir selalu merupakan pilihan yang lebih baik dan juga lebih populer.
file not found
#ifdef
Satu lagi perbedaan UNIX dan Windows adalah pustaka userspace yang kita gunakan untuk mengakses sistem. Pada sistem UNIX, pustaka ruang pengguna yang paling penting adalah pustaka C (libc). Di Linux libc yang paling banyak digunakan adalah glibc, tetapi sebenarnya ada implementasi lain (misalnya musl libc). libc adalah lapisan antara kode kita dan kernel. Dalam tutorial ini semua fungsi sistem UNIX yang kita gunakan diimplementasikan di dalam libc. Biasanya libc meneruskan permintaan kita ke kernel, tetapi terkadang ia menanganinya dengan sendirinya. Tanpa libc kita akan dipaksa untuk menulis lebih banyak kode untuk setiap OS (mengeksekusi syscalls sendiri), dan itu akan sangat sulit, memakan waktu dan tidak akan memberi kita manfaat nyata. Ini adalah titik di mana kita memutuskan lapisan tipis lintas platform kita harus ditempatkan, dan kita tidak perlu masuk lebih dalam.
Di Windows ada perpustakaan yang menyediakan fungsi bagi kita untuk mengakses sistem. kernel32 adalah jembatan antara userspace dan kernel. Seperti libc untuk UNIX, tanpa kernel32 kita perlu menulis lebih banyak kode (di atas ), dan biasanya kita tidak ingin melakukan itu.
kernel32.dll
ntdll.dll
Jadi secara umum cukup banyak yang harus kita tangani sekaligus saat menulis software lintas platform. Menggunakan fungsi pembantu atau pustaka diperlukan untuk menghindari kode yang terlalu kompleks dengan banyak ifdef-s dalam kode aplikasi. Dengan demikian, kita perlu mencari perpustakaan yang bagus atau menulis sendiri. Tetapi bagaimanapun juga kita harus benar-benar memahami apa yang terjadi di bawah tenda dan bagaimana kode aplikasi kita berinteraksi dengan sistem, sistem apa yang kita gunakan dan bagaimana. Ketika kami melakukannya, kami tumbuh dalam pengetahuan dan kami juga menulis perangkat lunak berkualitas lebih baik.
Tentang contoh
Sebelum kita mulai menyelami prosesnya, berikut adalah beberapa kata tentang contoh kode yang akan kita bahas di seluruh dokumen ini.
Kami menulis kode dalam fungsi hanya sekali dan berfungsi pada semua OS. Ini adalah ide kuncinya.
main()
Kode dalam menggunakan fungsi wrapper untuk setiap keluarga OS - di situlah kompleksitas dan perbedaan API sistem ditangani.
main()
Fungsi pembungkus tersebut sengaja dikurangi ukuran dan kerumitannya untuk tutorial ini - saya hanya menyertakan minimum yang diperlukan untuk contoh tertentu, tidak lebih.
Contoh yang saya berikan di sini sama sekali bukan kode nyata dan siap produksi. Saya membuatnya sederhana dan langsung ke intinya. Ide saya adalah pertama-tama Anda perlu memahami mekanisme kunci tentang cara bekerja dengan fungsi sistem dan cara mengelola kode lintas platform. Anda harus membaca lebih banyak kode dan akan lebih sulit bagi saya untuk menjelaskan semuanya sekaligus jika saya memilih pendekatan lain.
Untuk membangun file contoh di UNIX, jalankan saja file . File biner akan dibuat di direktori yang sama. Anda perlu menginstal dan atau . Di Windows Anda harus mengunduh paket MinGW dan menginstalnya, lalu jalankan .
make
make
gcc
clang
mingw64-make.exe
Jika Anda ingin menganalisis implementasi lengkap dari setiap fungsi pembungkus yang kita diskusikan di sini, Anda selalu dapat melihat / mengkloning perpustakaan saya ffbase & ffos, mereka benar-benar gratis. Untuk kenyamanan Anda, ada tautan langsung ke kode mereka yang ditempatkan setelah setiap bagian dalam tutorial ini.
Saat membaca contoh, saya sarankan Anda juga membaca dokumentasi resmi untuk setiap fungsi. Untuk sistem UNIX ada halaman manual, dan untuk Windows ada situs Microsoft Docs.
Alokasi Memori
Hal terpenting yang kita butuhkan saat menulis program - mengalokasikan memori untuk variabel dan array kita. Kita dapat menggunakan memori tumpukan untuk data operasi kecil atau kita dapat secara dinamis mengalokasikan wilayah memori besar menggunakan pengalokasi memori tumpukan. libc menyediakan antarmuka yang mudah untuk melakukan itu dan kita akan belajar cara menggunakannya. Tetapi sebelum itu kita harus memahami bagaimana memori tumpukan berbeda dari memori tumpukan.
Memori tumpukan
Stack memory adalah buffer yang dialokasikan oleh kernel untuk program kita sebelum mulai dijalankan. Program C mencadangkan ("mengalokasikan") wilayah memori dari tumpukan seperti:
int i; // reserve +4 bytes on stack
char buffer[100]; // reserve +100 bytes on stack
Selama proses kompilasi, complier mencadangkan beberapa ruang tumpukan yang diperlukan agar fungsi dapat berjalan dengan benar. Ini menempatkan beberapa instruksi CPU di awal setiap fungsi. Instruksi ini hanya mengurangi jumlah byte yang diperlukan dari pointer ke wilayah tumpukan (stack pointer). Kompiler juga menempatkan beberapa instruksi yang mengembalikan stack pointer ke keadaan sebelumnya ketika fungsi kita keluar - dengan cara itu kita membebaskan area tumpukan yang disediakan oleh fungsi kita sehingga wilayah yang sama dapat digunakan oleh beberapa fungsi lain setelah kita. Ini berarti bahwa fungsi kita tidak dapat mengembalikan pointer ke buffer stack, karena area yang sama dapat digunakan kembali / ditimpa.
Misalkan kita memiliki program seperti ini:
void bar()
{
int b;
return;
}
void foo()
{
int f;
bar();
return;
}
void main()
{
int m;
foo();
}
Untuk program di atas ada 5 status bagaimana memori tumpukan kita akan terlihat (sangat disederhanakan, tentu saja) selama eksekusi program:
Kami berada di dalam fungsi di baris. Kompiler telah memesan memori tumpukan untuk variabel kita, itu ditampilkan dalam warna abu-abu. Garis hijau adalah penunjuk tumpukan saat ini yang bergerak ke bawah ketika kita memesan beberapa byte lagi di tumpukan, dan bergerak ke atas ketika kita membebaskan wilayah yang dipesan tersebut. Kami memanggil .
main()
foo();
m
foo()
Kami berada di dalam fungsi, dan lebih banyak ruang tumpukan disediakan untuk variabel kami. Semua data yang dicadangkan oleh disimpan di wilayah tumpukan di atas milik kami. Kami memanggil .
foo()
f
main()
bar()
Di dalam, bahkan lebih banyak ruang tumpukan digunakan untuk menampung variabel kita. Wilayah yang dicadangkan oleh semua fungsi induk dipertahankan. Kami dari fungsi. Pada titik ini area tumpukan yang disediakan untuk variabel akan dibuang dan sekarang dapat digunakan kembali oleh fungsi lain.
bar()
b
return
b
Kami kembali masuk dan dari sana. Hal yang sama terjadi sekarang dengan wilayah tumpukan - area yang disediakan untuk kami dibuang.
foo()
return
foo()
f
Kami kembali ke . Hanya area untuk variabel yang masih berlaku di stack.
main()
m
Memori tumpukan terbatas, dan ukurannya cukup rendah (paling banyak beberapa megabyte). Jika Anda memesan sejumlah besar byte pada tumpukan, program Anda mungkin macet ketika Anda akan mencoba mengakses area yang dialokasikan di luar batas buffer tumpukan (yaitu area di bawah garis merah). Dan kita tidak dapat menambahkan lebih banyak ruang tumpukan saat program kita sedang berjalan. Selain itu, menggunakan stack untuk array dan string secara sembarangan dapat menyebabkan masalah keamanan yang parah (kondisi stack overflow, ketika dieksploitasi oleh penyerang, dapat dengan mudah mengakibatkan eksekusi kode arbitrer).
Memori tumpukan
Apa yang kita inginkan adalah mekanisme yang memungkinkan kita untuk secara dinamis mengalokasikan buffer memori besar dan mengubah ukurannya - inilah gunanya heap memory. Bagaimana memori heap berbeda dari stack:
Kami dapat dengan aman mengalokasikan buffer besar pada tumpukan, selama ada sumber daya sistem yang cukup.
Kami dapat mengubah ukuran buffer tumpukan kapan saja.
Fungsi kita dapat dengan aman mengembalikan pointer ke buffer heap apa pun, dan area ini tidak akan secara otomatis digunakan kembali / ditimpa oleh fungsi eksekusi berikutnya.
3 langkah cara menggunakan memori heap:
Kami meminta libc untuk mengalokasikan beberapa memori untuk kami. libc, pada gilirannya, meminta OS untuk memesan area memori dari RAM atau file swap.
Kemudian kita bisa menggunakan buffer ini selama yang kita butuhkan.
Ketika kita tidak membutuhkannya lagi, kita membebaskan area memori ini dengan memberi tahu libc. Ini mengembalikan buffer kembali ke OS sehingga dapat memberikan area memori yang sama untuk beberapa proses lainnya.
Algoritma libc biasanya pintar dan tidak akan mengganggu kernel setiap kali kita mengalokasikan atau membebaskan buffer heap. Sebagai gantinya, ia dapat menyimpan penyangga besar dan membaginya menjadi potongan-potongan, lalu mengembalikan potongan-potongan itu secara terpisah kepada kami. Juga, ketika program kita membebaskan buffer kecil, itu tidak berarti bahwa itu dikembalikan ke kernel, itu tetap di-cache di dalam libc.
Misalkan kita memiliki kode seperti ini:
#include <stdlib.h>
void main()
{
void *m1 = malloc(1);
void *m2 = malloc(2);
void *m3 = malloc(3);
free(m2);
free(m1);
free(m3);
}
Berikut adalah bagaimana wilayah memori tumpukan yang sebenarnya mungkin terlihat seperti (sangat disederhanakan):
Ketika kami mengalokasikan blok baru, libc meminta OS untuk mengalokasikan wilayah memori untuk kami. Kemudian libc mencadangkan jumlah ruang yang dibutuhkan dan mengembalikan pointer ke potongan ini kepada kami.
Ketika kami meminta lebih banyak buffer heap, libc menemukan potongan gratis di dalam wilayah heap dan mengembalikan pointer memori baru kepada kami. libc tidak akan meminta OS untuk mengalokasikan lebih banyak memori untuk kita sampai benar-benar diperlukan.
Ketika kita meminta untuk membebaskan buffer, libc hanya menandainya sebagai "gratis". Buffer lainnya tetap apa adanya.
Setelah semua buffer dibebaskan, libc dapat mengembalikan area memori kembali ke OS (tetapi tidak harus).
Bagaimana libc mengalokasikan atau membatalkan buffer, bagaimana ia menemukan potongan gratis, dll. - Ini tidak terlalu penting bagi kami, kami hanya memiliki antarmuka sederhana yang menyembunyikan semua kompleksitas dari kami.
Memori habis
Ketika kami meminta OS untuk mengalokasikan bagi kami jumlah memori yang lebih besar dari yang tersedia secara fisik saat ini, OS dapat mengembalikan kesalahan kepada kami, menunjukkan bahwa permintaan kami untuk buffer sebesar ini tidak dapat dipenuhi. Dalam situasi ini, jika kita menulis aplikasi ramah pengguna yang baik, kita mungkin harus mencetak pesan kesalahan yang bagus tentang itu dan meminta pengguna apa yang harus dilakukan selanjutnya. Namun, pada kenyataannya ini jarang terjadi dan dibutuhkan terlalu banyak upaya untuk menangani kasus-kasus memori yang tidak mencukupi dengan benar, bahwa biasanya aplikasi hanya mencetak pesan kesalahan dan kemudian crash. Namun, akan sangat disayangkan jika pengguna kehilangan jam kerja yang belum disimpan (misalnya file teks yang belum disimpan) saat menggunakan aplikasi kami. Kita harus berhati-hati tentang itu.
Ketika Linux mencadangkan wilayah memori untuk kita, Linux tidak segera mencadangkan jumlah memori fisik yang sama. Anda dapat melihat bahwa jumlah memori nyata yang dikonsumsi oleh proses yang baru saja mengalokasikan buffer tumpukan 4GB tidak banyak berubah. Linux mengasumsikan bahwa meskipun proses kami dapat meminta buffer besar, pada kenyataannya kami mungkin tidak membutuhkan banyak ruang. Kecuali kita benar-benar menulis data ke wilayah memori itu, blok memori fisik tidak akan dialokasikan untuk kita. Ini berarti bahwa beberapa proses yang berjalan pada sistem secara paralel dapat meminta blok memori besar, dan semua permintaan mereka akan dipenuhi, bahkan jika tidak ada cukup memori fisik untuk menyimpan semua data mereka. Apa yang akan terjadi jika semua proses mulai menulis data aktual ke buffer mereka sekaligus? Subsistem Out-Of-Memory (OOM) yang berjalan di dalam kernel hanya akan membunuh salah satunya ketika batas memori fisik tercapai. Jadi apa artinya bagi kita? Ingatlah bahwa ketika kita mengalokasikan buffer besar di Linux, proses kita terkadang bisa terbunuh ketika kita akan mencoba mengisi buffer tersebut dengan data. Biasanya, aplikasi kita harus bagus untuk semua aplikasi lain yang berjalan di sistem pengguna, dan jika kita memerlukan jumlah memori yang sangat besar untuk pekerjaan kita, kita harus berhati-hati untuk menghindari situasi OOM seperti itu, terutama jika pengguna memiliki beberapa pekerjaan yang belum disimpan.
Menggunakan Heap Buffer
OK, sekarang mari kita lihat contoh yang mengalokasikan buffer pada tumpukan dan kemudian segera membebaskannya.
Gulir ke bawah ke fungsi kami. Berikut adalah pernyataan yang mengalokasikan buffer tumpukan 8MB:main()
void *buf = heap_alloc(8*1024*1024);
Kami memanggil fungsi kami sendiri (kami akan segera membahas implementasinya) dengan parameter tunggal - jumlah byte yang ingin kami alokasikan pada tumpukan. Hasilnya adalah penunjuk ke awal buffer ini. Ini berarti bahwa kita memiliki wilayah memori 8MB yang tersedia untuk membaca dan menulis. Biasanya, pointer ini sudah selaras dengan setidaknya 4 atau 8 byte (tergantung pada arsitektur CPU). Sebagai contoh, kita dapat langsung unreference atau pointer di alamat ini bahkan pada ARM 32-bit:heap_alloc()
[buf..buf+8M)
short*
int*
int *array = heap_alloc(8*1024*1024);
array[0] = 123; // should work fine on ARM
Satu hal lagi yang penting: tidak ada yang menghentikan kita dari membaca atau bahkan menulis beberapa data melewati batas-batas buffer. Misalnya, dalam contoh kita di sini, kita sebenarnya dapat mencoba menulis lebih dari 8MB data ke dalam buffer itu dan kemungkinan besar kita akan berhasil melakukannya. Namun, bencana dapat terjadi kapan saja, karena kami secara tidak sengaja dapat menimpa data buffer tumpukan kami yang lain. Seluruh wilayah memori tumpukan mungkin menjadi rusak setelah itu. Jika kita mencoba mengakses data lebih jauh, kita dapat melewati garis kritis di mana ruang memori yang belum dipetakan dimulai. Dalam hal ini CPU akan memicu pengecualian dan program kita akan crash. Jadi ini berarti bahwa ketika bekerja dengan buffer di C kita harus selalu melewatkan kapasitasnya sebagai parameter fungsi (atau di ) sehingga tidak ada fungsi kita yang akan mengakses data melewati ujung buffer. Jika Anda sedang menulis program dan Anda mengalami crash mendadak, kemungkinan besar kode Anda secara tidak sengaja menimpa tumpukan atau buffer tumpukan di suatu tempat. Jika itu masalahnya, Anda dapat mencoba mengkompilasi aplikasi Anda dengan opsi yang akan mencetak pesan bagus tentang di mana Anda membuat kesalahan. Biasanya membantu.struct
-fsanitize=address
Baris berikutnya:
assert(buf != NULL);
Pernyataan ini akan crash program kita jika memori sistem tidak cukup dan buffer kita belum dialokasikan. Dalam program sederhana kami benar-benar tidak ada lagi yang harus dilakukan, kami benar-benar membutuhkan penyangga itu... Namun, dalam perangkat lunak server, kita tidak boleh crash dalam kasus ini, tetapi sebaliknya menulis peringatan tentang situasi ini ke dalam file log dan kemudian melanjutkan operasi normal. Pada akhirnya, kami memutuskan apa yang harus dilakukan. Program C sangat fleksibel ketika beberapa hal tak terduga terjadi, program kami memiliki kontrol hampir mutlak atas sumber daya. Banyak bahasa pemrograman lain tidak memberikan fleksibilitas seperti itu, mereka hanya akan menghentikan proses tanpa memiliki kesempatan untuk menyimpan pekerjaan pengguna atau melakukan beberapa hal penting lainnya sebelum keluar.
Sekarang anggaplah kita menggunakan buffer kita untuk beberapa waktu dan melakukan beberapa pekerjaan penting (sebenarnya tidak ada yang bisa dilakukan dalam contoh kita di sini). Dan kemudian kami membebaskan buffer, mengembalikan wilayah memori yang dialokasikan kembali ke libc. Jika kita tidak membebaskan buffer heap yang dialokasikan, maka OS membebaskannya secara otomatis untuk kita ketika proses kita keluar. Karena itu, untuk program C sederhana Anda tidak benar-benar diharuskan untuk membebaskan semua pointer tumpukan. Tetapi jika Anda menulis program yang serius, dan penggunaan memori untuk aplikasi Anda akan terus bertambah, pengguna tidak akan senang dengan itu. Juga, kemungkinan besar aplikasi Anda akan mogok setelah beberapa waktu karena OOM. Membebaskan buffer tumpukan yang dialokasikan adalah suatu keharusan untuk perangkat lunak normal. Terkadang tampaknya sangat luar biasa untuk melacak setiap pointer yang Anda alokasikan, tetapi itu adalah harga yang kami bayar untuk kontrol 100% atas perangkat lunak kami. Karena itu, program C dapat bekerja pada sistem dengan jumlah memori yang tersedia sangat terbatas, program dalam bahasa lain tidak dapat menahan kondisi seperti itu. Saya berasumsi Anda sudah tahu teknik dengan di C atau di C ++ untuk secara efektif menangani pembebasan buffer tanpa masalah.goto end
auto_ptr<>
Itu saja untuk kode aplikasi kami! Sekarang mari kita bahas kode platform-dependent untuk UNIX dan Windows. Pertama, perhatikan bagaimana saya membagi kode dengan cabang:#ifdef-#else
#ifdef _WIN32
static inline void func()
{
...Windows-specific code...
}
#else // UNIX:
static inline void func()
{
...UNIX-specific code...
}
#endif
Saya menggunakan pendekatan yang sama di semua file sampel di sini. Selama beberapa tahun saya mencoba banyak pendekatan berbeda tentang cara mengelola kode lintas platform... Sekarang keputusan terakhir saya sebenarnya yang paling sederhana dan mudah: Saya hanya menggunakan fungsi inline statis (sehingga mereka tidak akan dikompilasi menjadi file biner kecuali digunakan) dan mendefinisikannya dalam file yang dibagi oleh 1 cabang tingkat atas. Saya ingin setiap contoh menjadi file tunggal lengkap tanpa arahan yang tidak perlu dan pada saat yang sama menyimpan kode di dalam tanpa cabang preprocessor.#ifdef
#include
main()
_WIN32
definisi preprocessor diatur secara otomatis ketika kita mengkompilasi untuk Windows - begitulah cara kompiler mengetahui cabang mana yang harus dipilih dan cabang mana yang harus diabaikan.
Fungsi heap pada Windows
OK, sekarang gulir ke atas ke cabang.#ifdef _WIN32
#include <windows.h>
Ini adalah file termasuk tingkat atas tunggal untuk API sistem Windows (ini mencakup banyak file lain di dalamnya, tapi itu bukan masalah kami). Hampir semua fungsi dan definisi yang diperlukan dideklarasikan dengan menyertakan . Bukan cara yang sangat efektif dalam hal kinerja kompilasi (untuk setiap unit kompilasi, preprocessor menganalisis puluhan file termasuk Windows), tetapi sangat sederhana dan sulit untuk dilupakan - mungkin menghemat waktu bagi programmer saat menulis kode. Jadi pada akhirnya, mungkin itu sebenarnya keuntungan besar?windows.h
Sekarang inilah fungsi alokasi heap untuk Windows:
void* heap_alloc(size_t size)
{
return HeapAlloc(GetProcessHeap(), 0, size);
}
HeapAlloc()
Mengalokasikan wilayah memori dengan ukuran yang diperlukan dan mengembalikan pointer ke awal buffer. Parameter pertama adalah heap handle (yaitu heap ID). Biasanya, kita hanya menggunakan yang mengembalikan deskriptor heap default untuk proses kita. Perhatikan bahwa parameter harus bertipe dan bukan karena pada sistem 64-bit kita mungkin ingin mengalokasikan wilayah memori yang sangat besar sebesar >4GB. Jenis bilangan bulat 32-bit tidak cukup untuk itu, maka file .GetProcessHeap()
size
size_t
int
size_t
Ini adalah bagaimana kita membebaskan buffer kita:
void heap_free(void *ptr)
{
HeapFree(GetProcessHeap(), 0, ptr);
}
Penunjuk yang kita lewati harus persis fungsi apa yang awalnya kembali kepada kita. Jangan melakukan operasi aritmatika pada pointer buffer tumpukan, karena begitu Anda kehilangannya, Anda tidak akan dapat membebaskannya dengan benar. Jika Anda perlu menambah pointer ini, lakukan dengan salinan (atau simpan yang asli di suatu tempat). Jika Anda akan mencoba untuk membebaskan pointer tidak valid, program mungkin macet.HeapFree()
HeapAlloc()
Seperti yang Anda lihat, nama-nama untuk fungsi kami hampir sama dengan fungsi Windows. Saya mengikuti aturan yang sama di mana-mana: setiap fungsi dimulai dengan konteksnya (dalam kasus kami - ), kemudian mengikuti kata kerja yang mendefinisikan apa yang kami lakukan dengan konteksnya. Di C, sangat nyaman untuk mengandalkan petunjuk otomatis yang ditampilkan editor kode kami saat kami menulis kode. Ketika saya ingin ada hubungannya dengan memori tumpukan, saya menulis dan segera editor kode saya menunjukkan kepada saya semua fungsi yang dimulai dengannya. Microsoft sebenarnya memiliki logika yang sama di sini dan mereka memiliki nama yang benar untuk kedua fungsi tersebut. Namun sayangnya, ini hanya pengecualian.
heap_
heap
HeapAlloc()/HeapFree()
Fungsi heap pada UNIX
Sekarang mari kita lihat bagaimana bekerja dengan tumpukan di UNIX.
#include <stdlib.h>
Pada sistem UNIX tidak ada file include tunggal. File ini menyediakan deklarasi untuk fungsi memori tumpukan serta beberapa tipe dasar ().size_t
Alokasi memori sederhana dan sederhana:
void* heap_alloc(size_t size)
{
return malloc(size);
}
Fungsi mengembalikan kesalahan, tetapi tidak selalu bergantung pada perilaku ini di Linux, karena aplikasi Anda mungkin macet saat menulis data aktual ke buffer yang dikembalikan oleh .NULL
malloc()
Membebaskan penunjuk buffer heap:
void heap_free(void *ptr)
{
free(ptr);
}
Seperti pada Windows, mencoba membebaskan pointer yang tidak valid dapat mengakibatkan crash proses. Mencoba membebaskan pointer adalah no-op, itu sama sekali tidak berbahaya.NULL
Seperti yang Anda lihat, nama-nama fungsi di UNIX sangat berbeda dari yang ada di Windows. Mereka tidak menggunakan camel-case, mereka sering sangat pendek (terlalu pendek, kadang-kadang), mereka bahkan tidak berbagi awalan atau akhiran yang sama. Menurut pendapat saya, kita harus membawa beberapa aturan dan logika di sini ... Jadi saya pikir nama fungsi saya yang dimulai dengan awalan lebih baik dan lebih jelas bagi saya dan juga bagi mereka yang membaca kode saya. Itu sebabnya saya memilih skema penamaan ini untuk semua fungsi, struktur, dan deklarasi saya lainnya - semuanya mengikuti aturan yang sama.
Alokasi objek pada tumpukan
Ketika kita mengalokasikan array data biasa pada tumpukan kita biasanya tidak peduli jika mereka mengandung beberapa data sampah, karena kita juga memiliki variabel yang disimpan secara terpisah untuk indeks / panjang array yang selalu 0 (array belum memiliki elemen yang valid). Kemudian, saat kita mengisi array, kita meningkatkan indeks dengan tepat, seperti:
int *arr = heap_alloc(100 * sizeof(int));
size_t arr_len = 0;
arr[arr_len++] = 0x1234;
Tidak mengganggu kami bahwa saat ini array kami memiliki 99 elemen yang tidak digunakan yang mengandung sampah. Namun, ketika kita mengalokasikan objek struktur baru, itu mungkin menjadi masalah:
struct s {
void *ptr;
};
...
struct s *o = heap_alloc(sizeof(struct s));
...
// Careful, don't accidentally use `o->ptr` as it currently contains garbage!
...
o->ptr = ...;
Ini mungkin terlihat tidak begitu penting pada pandangan pertama, tetapi dalam kode nyata dan kompleks itu adalah masalah yang sangat sangat menjengkelkan - secara tidak sengaja menggunakan beberapa data yang belum diinisialisasi di dalam objek C. Untuk menetralisir masalah potensial ini, kita dapat menggunakan fungsionalitas yang secara otomatis membersihkan buffer memori untuk kita:
#ifdef _WIN32
void* heap_zalloc(size_t n, size_t elsize)
{
return HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, n * elsize);
}
#else
void* heap_zalloc(size_t n, size_t elsize)
{
return calloc(n, elsize);
}
#endif
Parameter 1 adalah jumlah objek yang ingin kita alokasikan, dan parameter 2 adalah ukuran 1 objek. Kami menggunakan flag pada Windows yang memerintahkan OS untuk menginisialisasi konten buffer dengan sebelum mengembalikannya kepada kami.HEAP_ZERO_MEMORY
0x00
Sekarang kita dapat menggunakan fungsi kita untuk mengalokasikan objek dan segera nol isinya:
struct s *o = heap_zalloc(1, sizeof(struct s));
...
// If we accidentally access `o->ptr`, the program will either crash or do nothing.
Percayalah, tidak ada salahnya untuk secara otomatis menginisialisasi dengan nol isi dari semua objek C yang Anda alokasikan pada tumpukan atau tumpukan, dan trik kecil ini dapat menghemat waktu debugging dan melindungi kode Anda dari potensi masalah keamanan.
Realokasi buffer
Terkadang perlu untuk memasukkan beberapa elemen lagi ke dalam array yang dialokasikan pada tumpukan, yaitu kita ingin array kita tumbuh. Tetapi jika kami mencoba melakukannya segera tanpa meminta izin libc, kami dapat mengakses konten buffer saudara kami, yang akan mengakibatkan crash (dalam kasus terbaik). Jadi hal pertama yang harus kita lakukan adalah meminta libc untuk memberi kita pointer buffer baru yang cukup besar untuk menyimpan semua data yang kita inginkan. Kita membutuhkan fungsi yang mengambil 2 parameter: pointer ke array yang ada yang ingin kita perbesar dan ukuran barunya.heap_realloc()
#ifdef _WIN32
void* heap_realloc(void *ptr, size_t new_size)
{
if (ptr == NULL)
return HeapAlloc(GetProcessHeap(), 0, new_size);
return HeapReAlloc(GetProcessHeap(), 0, ptr, new_size);
}
#else
void* heap_realloc(void *ptr, size_t new_size)
{
return realloc(ptr, new_size);
}
#endif
libc mempertahankan data lama kami dalam jangkauan bahkan jika perlu menyalin data secara internal dari satu tempat ke tempat lain. Perhatikan bahwa fungsi kami juga mendukung kasus ketika yang berarti bahwa hanya buffer baru yang akan dialokasikan.[0..new_size)
ptr == NULL
Kesalahan umum saat menggunakan adalah mengganti pointer buffer dalam satu pernyataan seperti:realloc()
void *old = heap_alloc(...);
old = heap_realloc(old, new_size);
// ERROR, what if old == NULL now?
Pada kode di atas kita memiliki kebocoran memori, karena fungsi telah mengembalikan kesalahan dan pointer. Tetapi buffer yang dirujuk oleh pointer masih dialokasikan di dalam libc, dan sekarang tidak ada orang yang dapat membebaskannya karena kami baru saja mengatur pointer ke . Ini adalah kode yang benar menggunakan fungsi memory realloc:heap_realloc()
NULL
old
NULL
void *old = heap_alloc(...);
void *new_ptr = heap_realloc(old, new_size);
if (new_ptr == NULL) {
// handle error
return;
}
old = new_ptr;
Terlihat sedikit canggung, tapi ini aman.
Hasil
Jadi kami menulis beberapa fungsi yang menyediakan antarmuka lintas platform untuk bekerja dengan buffer tumpukan, dan kami menggunakannya untuk menulis kode kami untuk Windows dan UNIX tanpa -s. Anda telah belajar cara mengalokasikan buffer pada memori tumpukan, mengalokasikan ulang dan membebaskannya - ini adalah hal terpenting untuk setiap program.main()
#ifdef
Referensi: fungsi di ffbase/base.hffmem_*()
Deteksi Waktu Kompiler
Dalam contoh sebelumnya kami menggunakan definisi preprocessor untuk cabang antara Windows dan UNIX. Berikut adalah tabel untuk beberapa konstanta waktu kompilasi internal yang memungkinkan kita mendeteksi CPU dan OS target._WIN32
Mendeteksi CPU target:
Test Code
=================================
CPU is AMD64? #ifdef __amd64__
CPU is x86? #ifdef __i386__
CPU is ARM64? #ifdef __aarch64__
CPU is ARM? #ifdef __arm__
Mendeteksi OS target:
Test Code
=================================
OS is Windows? #ifdef _WIN32
OS is macOS? #if defined __APPLE__ && defined __MACH__
OS is Linux? #ifdef __linux__
OS is Android? #if defined __linux__ && defined ANDROID
OS is any UNIX? #ifdef __unix__
I/O Standar
Untuk aplikasi konsol, biasanya menggunakan fungsi libc seperti dan untuk menulis teks ke konsol. Fungsi-fungsi ini meneruskan data kami ke sistem (misalnya melalui UNIX), dan sistem mentransfer data ke proses lain yang bertanggung jawab untuk merender teks di layar. Membaca dan menulis data dari/ke konsol dilakukan melalui deskriptor standar. Secara default, untuk setiap proses ada 3 di antaranya:puts()
printf()
write()
Standard Input Descriptor (STDIN) - digunakan untuk membaca beberapa data input;
deskriptor output standar (stdout) - digunakan untuk menulis beberapa data output;
Standard Error Descriptor (Stderr) - digunakan untuk menulis beberapa data output (biasanya, peringatan atau pesan kesalahan).
Tidak perlu menyiapkannya sebelum menggunakan. Ketika program kami dimulai, deskriptor siap digunakan.
Program gema sederhana
Ini adalah program yang sangat sederhana yang membaca beberapa teks dari pengguna, dan kemudian mencetak teks yang sama kembali kepadanya. Untuk menutup program yang sedang berjalan pengguna dapat menekan .Ctrl+C
Gulir ke bawah ke . Pertama, kita membaca beberapa teks dari pengguna:main()
char buf[1000];
ssize_t r = stdin_read(buf, sizeof(buf));
if (r <= 0)
return;
Kami memiliki buffer di tumpukan dan kami meneruskannya ke yang merupakan fungsi lintas platform kami untuk membaca dari stdin. Fungsi kami mengembalikan jumlah byte yang dibaca, ketika semua data input dikonsumsi, atau karena kesalahan. Ketika pengguna menekan sambil menunggu inputnya, fungsi akan mengembalikan kesalahan. Jika pengguna menekan , fungsi akan kembali . Juga, tidak apa-apa untuk menguji kesalahan dengan , daripada , karena tidak mungkin memaksa fungsi yang mendasarinya untuk mengembalikan angka negatif lainnya.stdin_read()
0
-1
Ctrl+C
Ctrl+D
0
<0
==-1
read()
Sekarang kita hanya mencetak data yang sama kembali ke pengguna dengan menuliskannya ke stdout:
const char *d = ...;
while (r != 0) {
ssize_t w = stdout_write(d, r);
Fungsi mengembalikan jumlah byte yang ditulis atau pada kesalahan. Perhatikan bahwa ketika kembali dengan byte kurang ditulis dari yang awalnya kita minta, kita harus mengulangi prosedur lagi sampai kita menulis semua byte dari buffer kita. Itu sebabnya kita perlu loop di sini.-1
stdout_write()
buf
Sekarang mari kita bahas implementasi fungsi pembantu kita.
I/O Standar pada UNIX
Gulir ke cabang UNIX. Kode ini sangat mudah pada sistem UNIX:
ssize_t stdin_read(void *buf, size_t cap)
{
return read(STDIN_FILENO, buf, cap);
}
ssize_t stdout_write(const void *data, size_t len)
{
return write(STDOUT_FILENO, data, len);
}
Kami menggunakan 2 syscalls di sini: dan dengan parameter pertama menjadi deskriptor standar stdin atau stdout. stderr adalah , meskipun tidak tercakup dalam contoh kita.read()
write()
STDERR_FILENO
I/O Standar di Windows
Sekarang gulir ke atas ke cabang Windows. Seperti yang Anda lihat, kode untuk Windows tidak sekecil pada UNIX. Itu karena pada Windows kita harus secara manual menangani konversi pengkodean teks - kita ingin program kita berperilaku dengan benar ketika pengguna memasukkan teks Unicode. Dalam implementasi kami, hal pertama yang kami butuhkan adalah mendapatkan deskriptor input standar:stdin_read()
HANDLE h = GetStdHandle(STD_INPUT_HANDLE);
Maka kita memerlukan buffer karakter lebar terpisah untuk membaca data Unicode dari pengguna:
DWORD r;
wchar_t w[1000];
if (!ReadConsoleW(h, w, 1000, &r, NULL))
// error reading from console
Saya menggunakan nilai hardcoded untuk kapasitas buffer kami di sini dan saya bahkan tidak menggunakan konstanta - itu hanya untuk kesederhanaan. Dalam kode real kita mungkin akan menggunakan makro (dievaluasi sebagai ) yang mengembalikan jumlah karakter lebar dalam buffer kita. Karena fakta bahwa fungsi beroperasi dengan charaters lebar dan bukan byte, kami melewatkan kapasitas buffer kami dalam charaters lebar (bukan byte), sehingga saja akan menjadi kesalahan. Saat kembali, fungsi mengisi buffer kami dengan data pengguna dan mengatur jumlah karakter lebar yang dibaca. (Info lebih lanjut tentang karakter lebar di Windows ada di bab berikutnya.) Jika fungsi gagal, ia mengembalikan .sizeof(w) / sizeof(*w)
ReadConsoleW()
sizeof(w)
0
Pada Windows beberapa fungsi seperti menggunakan tipe data yang salah untuk kapasitas buffer pengguna - , yaitu . Ini salah pada sistem 64-bit, karena ukuran jenisnya hanya 32-bit. Mengapa ini menjadi masalah? Karena jika kita mengalokasikan wilayah memori yang besar, misalnya tepatnya 4GB, maka ketika kita meneruskan angka ini ke , kompiler hanya akan memotong nilai kita menjadi . Akibatnya, kode kita tidak akan berfungsi sama sekali dalam beberapa kasus - itu tergantung pada kapasitas buffer, yang merupakan hal yang terkadang tidak kita kendalikan sepenuhnya saat runtime. Oleh karena itu, ketika kita melewatkan jumlah byte yang tersedia di buffer kita ke fungsi Windows, dan jika jenis parameternya adalah dan tidak , kita harus selalu menggunakan notasi untuk menghindari masalah. Saya pikir hanya beberapa orang yang benar-benar peduli tentang hal itu dalam kenyataan, tetapi jika kita menulis perpustakaan kita harus siap untuk semua jenis skenario dan tidak hanya mengandalkan keberuntungan kita. Adapun saran lain: jangan gunakan ketik kode Anda, karena itu bukan lintas platform. Untuk ukuran buffer selalu ada yang 32-bit (yaitu) atau 64-bit (yaitu) tergantung pada CPU, tidak peduli OS.ReadConsoleW()
DWORD
unsigned long
ReadConsoleW()
0
DWORD
size_t
min(cap, 0xffffffff)
long
size_t
unsigned int
unsigned long long
Langkah selanjutnya adalah mengonversi teks yang dikembalikan oleh , yaitu , ke format kita . Kita dapat menggunakan fungsi Windows untuk ini, kita tidak diharuskan untuk menulis kode konversi sendiri.ReadConsoleW()
wchar_t[]
char[]
WideCharToMultiByte(CP_UTF8, 0, w, r, buf, cap, NULL, NULL);
Kami meneruskan buffer karakter lebar kami yang diisi dengan data pengguna ke fungsi ini. Kami juga melewati buffer yang telah kami alokasikan sebelumnya dalam fungsi kami - buffer ini adalah tempat kami ingin menulis teks dengan pengkodean yang benar. Fungsi mengembalikan jumlah byte yang ditulis atau pada kesalahan. Fungsi ini dijelaskan secara lebih rinci nanti.main()
WideCharToMultiByte()
0
Sekarang perhatikan fungsinya . Algoritmanya adalah pertama-tama kita mengubah data UTF-8 pengguna menjadi UTF-16 di dalam buffer terpisah, kemudian kita memanggil fungsi penulisan konsol untuk mencetaknya di layar. Tetapi sebelum itu kita harus mendapatkan deskriptor yang diperlukan dari sistem. Untuk mendapatkan stdout kami melakukan:stdout_write()
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
Dan ini untuk stderr (tidak tercakup dalam contoh ini):
HANDLE h = GetStdHandle(STD_ERROR_HANDLE);
Kami mengonversi data UTF-8 ke format karakter lebar seperti:
wchar_t w[1000];
int r = MultiByteToWideChar(CP_UTF8, 0, data, len, w, 1000);
Dan teruskan data karakter lebar ke sistem:
DWORD written;
if (!WriteConsoleW(h, w, r, &written, NULL))
// error writing to console
Tetapi mengapa kita mengabaikan nilai? Karena itu akan menjadi masalah bagi kita untuk menggunakan nilai ini jika kembali sebelum menulis semua data kita. Kami tidak dapat dengan cepat mendapatkan posisi dalam teks UTF-8 kami pada sejumlah karakter lebar tertentu. Namun, dalam praktiknya, sistem tidak akan kembali dari fungsi ini kecuali berhasil mentransfer semua data kami. Desain fungsi kami tidak benar untuk kasus ini. Jadi pada akhirnya saya pikir cukup aman untuk mengasumsikan ini dan mengabaikan nilai.
written
WriteConsoleW()
stdout_write()
written
Standar I / O: pengalihan
Sekarang lihat implementasi kami dan untuk Windows sekali lagi - kami belum membahas beberapa kode. Ini diperlukan untuk menangani pengalihan deskriptor standar dengan benar. Biarkan saya menjelaskan bagaimana mekanisme ini bekerja pada tingkat tinggi pada UNIX.stdin_read()
stdout_write()
Pertama, mari kita kompilasi dan jalankan contoh kita:
./std-echo
Program ini menunggu masukan kami. Kami masuk dan tekan Enter:hello!
hello!
hello!
Kami melihat bahwa program telah segera mencetak kepada kami baris yang sama yang baru saja kami masukkan. Semuanya bagus sejauh ini. Tetapi kadang-kadang kita ingin menghubungkan dua program bersama-sama sehingga satu dapat meneruskan beberapa teks ke yang lain. Dalam hal ini kami menggunakan operator seperti ini:|
$ echo hello | ./std-echo
hello
Di sini kita menjalankan 2 program, dan yang pertama () meneruskan teks ke program kita, yang akan mencetaknya ke konsol. Program kami tidak membaca input dari pengguna, melainkan membacanya dari program lain. Diagram menunjukkan bagaimana data benar-benar dialihkan:echo hello
hello
[bash]
-> pipe(W) -> pipe(R) -
[echo] / \ [std-echo]
"hello" -> stdout - -> stdin -> "hello"
bash adalah program shell yang menggunakan pipa untuk mentransfer data dari ke file . Kami belum tahu apa-apa tentang pipa, mereka akan dijelaskan nanti.echo
std-echo
Sekarang contoh yang lebih kompleks dengan 3 program yang terhubung bersama:
$ echo hello | ./std-echo | cat
hello
Kali ini program kita tidak akan mencetak teks ke konsol itu sendiri, melainkan outputnya akan dialihkan ke program lain ().cat
Ketika deskriptor standar dialihkan, mereka merujuk ke pipa dan bukan konsol. Di UNIX ini tidak mengganggu kami, karena kode kami berfungsi untuk semua kasus secara otomatis. Namun, pada Windows kita harus melakukan konversi teks untuk mendukung Unicode ketika deskriptor standar adalah konsol, tetapi kita tidak perlu melakukan konversi teks apa pun ketika deskriptor standar adalah pipa. Berikut algoritmanya:
Ketika Stdin adalah konsol - kami menggunakan
ReadConsoleW()
Ketika stdin adalah pipa - kami menggunakan
ReadFile()
Ketika stdout/stderr adalah konsol - kami menggunakan
WriteConsoleW()
Ketika stdout / stderr adalah pipa - kami menggunakan
WriteFile()
Meskipun ini meminta beberapa kode tambahan dari kami, semua hal ini disembunyikan dari pengguna di perpustakaan kami, jadi pada akhirnya itu bukan masalah besar. Ini adalah cara kami memeriksa apakah deskriptor adalah konsol atau tidak:
DWORD r;
HANDLE h = GetStdHandle(...);
if (GetConsoleMode(h, &r))
// this is a console descriptor
Fungsi kembali jika kita meneruskan deskriptor konsol ke sana, dan ketika deskriptor adalah pipa. Setelah fungsi mengonfirmasi bahwa ini adalah konsol, kami melanjutkan dengan memanggil seperti yang saya jelaskan di atas secara rinci. Tetapi ketika deskriptor stdin kita adalah pipa, kita harus menggunakan fungsi lain:GetConsoleMode()
1
0
ReadConsoleW()
void *buf = ...;
size_t cap = ...;
DWORD read;
if (!ReadFile(h, buf, cap, &read, 0))
// error reading from a pipe
ReadFile()
adalah fungsi umum yang membaca beberapa data dari deskriptor file (atau pipa) apa pun, ini akan mentransfer data file UTF-8 pengguna dengan benar ke program kami. Fungsi ini mengatur jumlah byte yang benar-benar dibaca dan kembali pada keberhasilan atau kesalahan.1
0
Inilah cara kami menulis ke stdout / stderr jika mereka merujuk ke pipa:
const void *data = ...;
size_t len = ...;
DWORD written;
if (!WriteFile(h, data, len, &written, 0))
// error writing to a pipe
WriteFile()
adalah fungsi umum yang menulis beberapa data ke deskriptor file (atau pipa) apa pun tanpa konversi pengkodean teks. Dengan kata lain, jika kita meneruskan data UTF-8 ke sana, data ini akan ditulis dengan benar ke target, misalnya file UTF-8 atau pipa. Fungsi ini mengatur jumlah byte yang benar-benar ditulis dan mengembalikan keberhasilan atau kesalahan.1
0
Hasil
Kami telah belajar cara menggunakan deskriptor standar untuk membaca atau menulis data ke/dari konsol atau program lain.
Referensi: FFOS/std.h
Pengkodean &; Konversi Data
Seperti yang sudah Anda ketahui, pengkodean teks default pada Windows adalah UTF-16LE, sedangkan pada UNIX biasanya UTF-8 yang jauh lebih baik. Baik atau buruk, kita masih perlu memiliki antarmuka yang identik dan lintas platform. Jadi pada Windows kita harus menulis beberapa kode untuk mengubah teks menjadi / dari UTF-16LE di dalam setiap fungsi yang beroperasi dengan teks.
Jika kita akan mengalokasikan buffer memori heap baru untuk setiap panggilan ke fungsi perpustakaan kita, kinerjanya mungkin sedikit turun karena penggunaan alokasi heap libc yang berlebihan. Jadi untuk mengurangi jejak di perpustakaan ffos saya, pertama-tama saya mencoba menggunakan buffer kecil di tumpukan dan kemudian, jika itu tidak cukup, saya mengalokasikan buffer dengan ukuran yang diperlukan pada tumpukan. Anda dapat menganalisis mekanisme ini jika Anda mau, tetapi untuk saat ini kami tidak peduli dengan kinerjanya, dan dengan demikian contoh kami sangat sederhana - kami hanya menggunakan buffer pada tumpukan dan tidak peduli apakah mereka cukup untuk menyimpan semua data atau tidak.
OK, jadi apa sebenarnya UTF-16LE itu? Ini adalah pengkodean di mana setiap karakter membutuhkan 2 atau 4 byte ruang. Postfix berarti endian rendah. Angka low-endian berarti bahwa 8 bit rendah ditulis ke byte pertama, dan 8 bit tinggi - ke byte kedua (misalnya kode karakter spasi adalah , atau , dan dalam UTF-16LE akan direpresentasikan sebagai ). Jelas, pengkodean ini tidak memetakan ke UTF-8 apa adanya sehingga kita membutuhkan konvertor. Untuk mempermudah, kami akan menggunakan fungsi yang disediakan Windows di luar kotak.LE
0x20
0x0020
0x20 0x00
Kode ini mengubah teks UTF-8 kami menjadi UTF-16LE:
char *utf8_data = ...;
unsigned int utf8_data_len = ...;
wchar_t w[1000];
int wide_chars = MultiByteToWideChar(CP_UTF8, 0, utf8_data, utf8_data_len, w, 1000);
Kami memesan buffer karakter lebar pada tumpukan dan meminta Windows untuk mengonversi data panjang UTF-8 kami yang dikodekan . Hasilnya akan disimpan dalam buffer kapasitas sama dengan karakter lebar. Nilai yang dikembalikan adalah jumlah karakter aktual yang telah ditulis atau kesalahan fungsi.utf8_data
utf8_data_len
w
1000
0
Hati-hati: byte, karakter, dan buffer karakter lebar adalah istilah yang berbeda:
Untuk tipe UTF-8 digunakan, tetapi hanya mewakili satu byte, bukan karakter penuh.
char
Karakter UTF-8 terdiri dari 1..7 byte (meskipun, saat mengonversi dari UTF-16, nomor UTF-8 maks. hanya 4 byte).
Karakter UTF-16 adalah 2 atau 4 byte.
wchar_t
adalah tipe C yang disebut "karakter lebar": di Linux ukurannya 4 byte, di Windows - 2 byte. tidak relevan dengan UTF-16 atau pengkodean teks apa pun - ini hanya jenis untuk mengakses data.wchar_t
Sekarang, kode ini melakukan konversi terbalik, teks UTF-16LE Windows ke UTF-8:
wchar_t w[1000];
unsigned int w_len = SomeWindowsFunctionW(..., w, 1000);
char *utf8_buf[1000 * 4];
int bytes = WideCharToMultiByte(CP_UTF8, 0, w, w_len, utf8_buf, sizeof(utf8_buf), NULL, NULL);
Kami:
Cadangan buffer karakter lebar di tumpukan.
Panggil beberapa fungsi Windows yang akan menulis beberapa data ke buffer ini; Fungsi biasanya mengembalikan jumlah karakter lebar yang sebenarnya ditulis, kami menyimpannya di .
w_len
Cadangan buffer UTF-8 yang dapat menampung hingga 1000 karakter dari UTF-16.
Kemudian kami mengubah UTF-16LE menjadi UTF-8: panjangnya menjadi buffer . Nilai yang dikembalikan adalah jumlah byte aktual yang telah ditulis atau kesalahan fungsi.
w
w_len
utf8_buf
0
Terkadang nyaman untuk beroperasi pada string yang diakhiri NULL tanpa perlu menentukan panjangnya terlebih dahulu. Keduanya dan fungsi mendukung ini. Setiap kali kita ingin mereka mengonversi string yang diakhiri NULL, kita hanya meneruskan alih-alih panjang string yang sebenarnya, dan mereka akan secara otomatis menghentikan pemrosesan setelah karakter NULL ditulis. Dalam hal ini, nilai yang dikembalikan juga akan menyertakan karakter NULL.WideCharToMultiByte()
MultiByteToWideChar()
-1
Hasil
Anda telah mempelajari cara menangani teks Unicode dengan benar di Windows dan mengubahnya menjadi / dari UTF-8.
Referensi: ffbase/unicode.h
File I / O: Program gema file sederhana
File adalah objek yang berisi beberapa data yang disimpan dalam sistem file. Sistem file (FS) adalah kumpulan data file dan meta data (properti file, izin akses, waktu file, dll.) yang biasanya disimpan pada beberapa disk. FS paling populer untuk Linux adalah ext4, untuk Windows - NTFS. Namun, ini tidak terlalu penting bagi kami, karena kami menggunakan fungsi API sistem yang identik untuk semua FS. File dapat dari berbagai jenis: file biasa, direktori, tautan simbolik dan keras. Kita dapat membuat / menghapus file, melakukan operasi baca / tulis pada mereka, mendapatkan / mengatur properti mereka, mengubah nama mereka ... Direktori adalah file khusus yang berisi sekumpulan nomor ID file lainnya; kami tidak dapat melakukan I/O pada direktori.
Berikut adalah program yang sangat sederhana yang membaca beberapa data dari file, kemudian menulis data yang sama ke dalam file itu. Pengguna seharusnya membuat file teks kecil, dan program kami akan menambahkan teks yang sama ke dalamnya, seperti:
$ echo hello! >file-echo.log
$ ./file-echo
$ cat file-echo.log
hello!
hello!
Gulir ke bawah ke . Langkah pertama adalah membuka file yang ada untuk membaca dan menulis:main()
file f = file_open("file-echo.log", FILE_READWRITE);
assert(f != FILE_NULL);
Fungsi kami memiliki 2 parameter: jalur file (atau hanya nama) untuk file yang ingin kami buka dan bagaimana kami ingin membukanya (yaitu untuk membaca dan menulis). Fungsi ini mengembalikan deskriptor file yang akan kita gunakan untuk I / O, atau konstan pada kesalahan (kita akan berbicara tentang kesalahan sistem di bab berikutnya). Perhatikan bahwa jika kita akan mencoba mengeksekusi tanpa membuat file sebelumnya, pernyataan akan dipicu.FILE_NULL
file-echo
file-echo.log
Selanjutnya, kita membaca beberapa data dari file ini. I/O pada file praktis sama dengan I/O standar.
char buf[1000];
ssize_t r = file_read(f, buf, sizeof(buf));
Fungsi mengembalikan jumlah byte yang benar-benar dibaca atau kesalahan. Fungsi kembali jika akhir file tercapai dan tidak ada lagi data yang tersedia untuk dibaca. Perhatikan bahwa kita menggunakan buffer kecil dan melakukan satu panggilan ke fungsi membaca. Ini OK untuk contoh kecil kita, tetapi pada kenyataannya kita harus siap untuk menangani file yang lebih besar dari 1000 byte.-1
0
Kemudian kami menulis data ke file yang sama:
size_t buf_len = ...;
ssize_t r = file_write(f, buf, buf_len);
Setelah kami selesai bekerja dengan deskriptor file, kami menutupnya, sehingga sistem dapat membebaskan sumber daya yang dialokasikan:
file_close(f);
Setelah kami menutup deskriptor file, kami tidak dapat lagi menggunakannya. Jika kita mencoba, fungsi sistem akan mengembalikan kesalahan.
File I / O pada UNIX
Ini sangat mudah. Pertama, kami mendeklarasikan jenis lintas platform kami sendiri untuk deskriptor file:
typedef int file;
Ya, pada UNIX itu hanya bilangan bulat, mulai dari dan biasanya hanya bertambah 1 dengan setiap deskriptor file baru (nilai biasanya disediakan untuk 3 deskriptor standar). Setelah kami menutup beberapa deskriptor yang dibuka, nilainya dapat digunakan kembali nanti, tetapi kami tidak mengontrolnya - OS memutuskan nomor mana yang akan digunakan. Semua fungsi yang membuat deskriptor baru mengembalikan kesalahan. Itu sebabnya kita membutuhkan konstanta khusus untuk ini:0
0..2
-1
#define FILE_NULL (-1)
Fungsi yang membuka file di UNIX adalah . Parameter pertama adalah jalur file (absolut atau relatif terhadap direktori kerja saat ini). Parameter kedua adalah satu set bendera yang menentukan bagaimana kita ingin membuka file. Untuk contoh ini kita ingin membuka file untuk membaca dan menulis, sehingga kita menggunakan nilai. Saya tidak suka nama pendek bendera sistem di UNIX, jadi saya mencoba menggunakan penamaan yang lebih jelas untuk dipahami oleh programmer rata-rata.open()
O_RDWR
#define FILE_READWRITE O_RDWR
file file_open(const char *name, unsigned int flags)
{
return open(name, flags, 0666);
}
Ingat bahwa dalam contoh ini fungsi pembukaan file tidak akan membuat file baru jika tidak ada. Membuat file dibahas di bab berikutnya.
Kode lainnya sangat mudah:
int file_close(file f)
{
return close(f);
}
ssize_t file_read(file f, void *buf, size_t cap)
{
return read(f, buf, cap);
}
ssize_t file_write(file f, const void *data, size_t len)
{
return write(f, data, len);
}
File I/O di Windows
Kami memiliki beberapa kode lagi di setiap fungsi untuk Windows, seperti biasa. Implementasi dunia nyata pada Windows sedikit lebih besar dari ini. Pertama, kita membuat tipe baru untuk deskriptor file:file_open()
typedef HANDLE file;
Nilai untuk deskriptor file pada Windows tidak kecil, jumlahnya meningkat seperti pada UNIX, pikirkan tipe sebagai pointer yang secara unik mengidentifikasi deskriptor file kami.HANDLE
Ketika fungsi pembukaan file gagal, ia mengembalikan nilai khusus yang menunjukkan kesalahan - . Secara internal, ini dicor ke jenis pointer jadi jangan bingung dengan yang mana . Kami mendefinisikannya kembali sebagai berikut:INVALID_HANDLE_VALUE
-1
NULL
0
#define FILE_NULL INVALID_HANDLE_VALUE
Berikut fungsi yang membuka file yang ada di Windows:
#define FILE_READWRITE (GENERIC_READ | GENERIC_WRITE)
file file_open(const char *name, unsigned int flags)
{
wchar_t w[1000];
if (!MultiByteToWideChar(CP_UTF8, 0, name, -1, w, 1000))
return FILE_NULL;
unsigned int creation = OPEN_EXISTING;
unsigned int access = flags & (GENERIC_READ | GENERIC_WRITE);
return CreateFileW(w, access, 0, NULL, creation, FILE_ATTRIBUTE_NORMAL, NULL);
}
Karena kami menggunakan pengkodean UTF-8 untuk nama file, kami perlu mengonversinya menjadi UTF-16 sebelum diteruskan ke Windows. Jadi kami mengonversi teks dari ke buffer baru yang kemudian kami berikan ke Windows. flag berarti kita ingin membuka file yang ada dan tidak membuat yang baru. value menentukan bagaimana kita ingin mengakses file. Perhatikan bahwa untuk membuka file untuk membaca dan menulis, kita perlu menggabungkan 2 bendera bersama-sama. Jadi kita mengambil nilai bertopeng dari parameter user.name
w
OPEN_EXISTING
access
flags
Kami sudah tahu cara kerja Windows, jadi tidak ada yang perlu dijelaskan di sini:ReadFile/WriteFile
int file_close(file f)
{
return !CloseHandle(f);
}
ssize_t file_read(file f, void *buf, size_t cap)
{
DWORD rd;
if (!ReadFile(f, buf, cap, &rd, 0))
return -1;
return rd;
}
ssize_t file_write(file f, const void *data, size_t len)
{
DWORD wr;
if (!WriteFile(f, data, len, &wr, 0))
return -1;
return wr;
}
Fungsi I/O dan Kursor
Kita harus memahami bagaimana fungsi I/O sistem melacak posisi saat ini untuk setiap deskriptor file yang dibuka. Misalkan kita memiliki file dengan konten dan mengeksekusi file . Kami membaca 6 byte darinya dengan dan setelah kami menulis data yang sama dengan data file kami menjadi . Mengapa data baru ditambahkan sampai akhir dan kami tidak hanya menimpa "halo" dengan data yang sama? Itu karena kernel mempertahankan kursor untuk deskriptor file kami secara internal. Ketika kita membaca atau menulis dari / ke file, kursor ini selalu bergerak maju dengan jumlah byte yang ditransfer. Dengan kata lain, jika kita membaca hanya dengan 1 byte dari file, kursor file akan bergerak sebesar 1 byte setelah setiap operasi. Dengan demikian, kita dapat membaca seluruh file satu byte per satu dalam satu lingkaran, meskipun akan sangat tidak efektif, pola ini berfungsi. Dan hal yang sama berlaku untuk menulis ke file: dengan setiap operasi tulis kursor bergerak maju dengan jumlah byte yang ditulis. Posisi kursor disebut offset file dan itu hanya bilangan bulat 64-bit yang tidak ditandatangani. Kita dapat mengatur offset ke posisi apa pun yang kita butuhkan, jika kita mau.Hello!
std-echo
file_read()
file_write()
Hello!Hello!
Setelah kita membuka file yang berisi data, posisi kursornya awalnya , yang berarti kita berada di awal file dan kursor menunjuk ke byte:Hello!
0
H
Hello!
^
Jika kita membaca misalnya 2 byte, kursor akan bergerak maju 2 dan menunjuk ke byte:l
Hello!
^
Kami membaca beberapa data lagi dan akhirnya kami mencapai akhir file di mana kursor berada dan menunjuk ke ruang kosong:6
Hello!
^
Pada titik ini membaca dari file akan selalu menghasilkan kode pengembalian - kita tidak dapat membaca lebih banyak data karena tidak ada lagi data. Setelah kami menulis beberapa data ke file ini, kursor juga bergerak maju bersama kami:0
Hello!Hello!
^
Sekarang misalkan kita memberi tahu sistem untuk mengatur kursor kembali ke posisi lagi:2
Hello!Hello!
^
Dan tulis hal yang sama sekali lagi:Hello!
HeHello!llo!
^
Lihat bahwa kursor file diperbarui sesuai dan kami baru saja menimpa data lama dengan data baru. Jadi ketika bekerja dengan file, kita menganggapnya sebagai satu baris yang sangat besar di mana kita dapat memindahkan posisi kursor saat ini bolak-balik, membaca dan menulis data dari offset apa pun, menimpa data lama jika kita mau. Ketika kita menulis data ke file, kernel melakukan yang terbaik untuk mengikuti kita dan benar-benar memperbarui isi file pada perangkat penyimpanan fisik (data kita tidak harus ditransfer ke disk pada saat yang sama kita sebut fungsi menulis tetapi di-cache untuk sementara waktu di dalam kernel).
Ada situasi yang serupa tetapi sedikit berbeda untuk standar I / O atau deskriptor pipa. Mirip dengan file, setelah kita membaca beberapa data dari konsol atau pipa, operasi baca berikutnya tidak akan mengembalikan data lama kepada kita, karena sistem menggerakkan kursor internal ke depan setelah setiap I / O. Namun, setelah kita membaca dari deskriptor standar atau pipa, kita tidak dapat memindahkan kursor kembali seperti yang kita bisa dengan file, karena data itu sudah dikonsumsi, kita tidak dapat membacanya lagi. Hal yang sama berlaku untuk menulis ke stdout / stderr atau pipa: setelah kami menulis beberapa data, kami tidak dapat memindahkan kursor kembali dan mengubahnya, karena sudah ditransfer. Misalkan pengguna telah memasukkan "Halo!" dan menekan Enter. Buffer internal (dalam kernel) yang terkait dengan deskriptor kami akan terlihat seperti ini dengan kursor pada posisi:0
Hello!<LF>
^
Ketika kita membaca 2 byte dengan misalnya , kursor juga bergerak 2 byte ke depan, tetapi tidak seperti file, data yang dibaca menjadi tidak valid sehingga kita tidak dapat membacanya kembali.stdin_read()
..llo!<LF>
^
Seperti halnya file, setelah kursor mencapai akhir, fungsi membaca stdin kembali kepada kita, menunjukkan bahwa tidak ada lagi data:0
.......
^
Ketika pengguna memutuskan untuk memasukkan beberapa teks lagi, lebih banyak data dalam buffer ini akan tersedia bagi kita, tetapi kursor yang kita baca tidak akan berubah, tentu saja (karena kita belum membaca data baru ini).
.......Some new text<LF>
^
Bagi kami, programmer tingkat pengguna, buffer internal ini seperti garis tak terbatas dengan kursor selalu bergerak maju saat kami membaca huruf darinya. Untuk kernel, jelas, buffer memiliki batas tertentu dan kemungkinan besar diimplementasikan sebagai ring buffer, yang berarti bahwa setelah kursor mencapai akhir buffer, itu diatur ulang ke awal buffer.
Cari File/Potong
Sekarang setelah kita memahami cara kerja offset file, kita siap untuk contoh kode baru. Ini sedikit berbeda dari contoh sebelumnya: kita akan menimpa beberapa data yang ada dalam file dan memotong file sehingga ukurannya akan menjadi kurang dari sebelumnya. Misalnya, kita memiliki file dengan data. Kami membacanya ke buffer kami, lalu memindahkan kursor kembali ke awal dan menimpa data dengan paruh kedua, yaitu . Kemudian kita memanggil fungsi sistem untuk memotong file untuk kita. Akibatnya, sisa data dalam file kami akan dibuang.Hello!
lo!
Gulir ke dan lewati kode untuk dan karena kita sudah tahu cara kerjanya. Berikut adalah kode yang memindahkan kursor file ke awal file:main()
file_open()
file_read()
long long offset = file_seek(f, 0, FILE_SEEK_BEGIN);
assert(offset >= 0);
Parameter pertama adalah file descriptor, kemudian mengikuti angka offset absolut dan flag yang berarti bahwa kita ingin menetapkan posisi absolut dari awal file. Kita juga dapat mengubah offset file relatif terhadap nilai saat ini, atau dari ujung file, tetapi mereka tidak tercakup dalam contoh ini (saya bahkan berpikir bahwa menggunakan pendekatan ini adalah keputusan desain yang buruk). Fungsi mengembalikan offset file absolut baru atau kesalahan.FILE_SEEK_BEGIN
-1
Selanjutnya, kami menulis data ke file dan memotongnya dengan:
long long offset = ...;
assert(0 == file_trunc(f, offset));
Fungsi ini membuang semua data file setelah offset ini, hanya menyimpan byte. Ukuran file baru diatur sesuai. Jika offset lebih besar dari ukuran file saat ini, file diperluas. Fungsi ini juga berguna ketika Anda mengetahui terlebih dahulu ukuran file sebelum benar-benar menulis konten file - ini dapat membantu FS untuk lebih mengoptimalkan I / O dalam beberapa kasus.[0..offset)
Ketika saya menulis data ke file baru dan saya tidak tahu ukurannya sebelumnya, saya biasanya mengalokasikan ruang terlebih dahulu dengan faktor 2 - trik ini meminimalkan fragmentasi file untuk saya ketika menulis ke NTFS pada disk berputar (di Windows).
File Mencari / Memotong UNIX
Mengubah offset file disebut "mencari" pada file, berikut fungsinya:
#define FILE_SEEK_BEGIN SEEK_SET
long long file_seek(file f, unsigned long long pos, int method)
{
return lseek(f, pos, method);
}
Mungkin saja jika Anda menetapkan posisi di luar ukuran file di sini, tetapi jarang diperlukan.
Pemotongan juga cukup sederhana:
int file_trunc(file f, unsigned long long len)
{
return ftruncate(f, len);
}
Cari / Potong File di Windows
Fungsi pencarian file untuk Windows:
#define FILE_SEEK_BEGIN FILE_BEGIN
long long file_seek(file f, unsigned long long pos, int method)
{
long long r;
if (!SetFilePointerEx(f, *(LARGE_INTEGER*)&pos, (LARGE_INTEGER*)&r, method))
return -1;
return r;
}
SetFilePointerEx()
menginginkan nilai sebagai parameter yang dilemparkan ke bilangan bulat 64-bit tanpa masalah.LARGE_INTEGER
Fungsi pemotongan sedikit rumit dan tidak akan bekerja dengan andal jika digunakan secara sembarangan (dalam lingkungan multithreaded). Itu sebabnya Aku selalu mengatakan bahwa engkau harus benar-benar memahami bagaimana hal-hal bekerja di dalam sebelum menggunakannya secara membabi buta.
int file_trunc(file f, unsigned long long len)
{
long long pos = file_seek(f, 0, FILE_CURRENT); // get current offset
if (pos < 0)
return -1;
if (0 > file_seek(f, len, FILE_BEGIN)) // seek to the specified offset
return -1;
int r = !SetEndOfFile(f);
if (0 > file_seek(f, pos, FILE_BEGIN)) // restore current offset
r = -1;
return r;
}
Fungsi ini terdiri dari 4 langkah:
Dapatkan offset file saat ini
Atur kursor file ke posisi yang ditentukan pengguna
Potong file pada posisi saat ini
Memulihkan offset file sebelumnya
Agar untuk bekerja, pertama-tama kita harus mencari offset yang ditentukan. Tetapi setelah kembali dari fungsi kami, offset file saat ini tidak boleh diubah untuk kode pengguna, yang merupakan perilaku yang diharapkan. Perhatikan bahwa kita membutuhkan 4 sakelar konteks untuk melakukan operasi ini, sehingga tidak aman untuk menggunakan fungsi ini dari beberapa utas jika mereka menggunakan deskriptor file yang sama.SetEndOfFile()
Hasil
Kami telah belajar cara membuka file dan melakukan operasi baca / tulis / cari / potong pada mereka.
Referensi: ffos/file.h
Kesalahan Sistem
Banyak fungsi sistem mungkin gagal saat runtime karena berbagai alasan yang tidak dapat kami kontrol. Ketika mereka gagal, mereka biasanya menetapkan kode kesalahan sehingga kita dapat menentukan mengapa tepatnya gagal. Dalam aplikasi dunia nyata, menangani kesalahan dengan benar dan menampilkan pesan kesalahan / peringatan kepada pengguna adalah yang paling tidak bisa kita lakukan. Dalam contoh berikut, kami memaksa sistem untuk mengembalikan kode kesalahan kepada kami, lalu kami mendapatkan pesan kesalahan dan menunjukkannya kepada pengguna.
Pertama, ini adalah bagaimana kita dapat memaksa fungsi sistem untuk mengembalikan kesalahan kepada kita.
int r = file_close(FILE_NULL);
DIE(r != 0);
Kami sengaja menggunakan deskriptor file yang tidak valid dan mencoba mengoperasikannya. Jelas, fungsi gagal dan mengembalikan nilai bukan nol. Setelah kembali, fungsi juga menetapkan nomor kesalahan dalam variabel global. Kondisi kesalahan pemeriksaan makro kami, dan jika demikian, ia membaca nomor kesalahan terakhir dari variabl global, mencetak pesan kesalahan dan mengakhiri proses. Untuk mengambil pesan kesalahan, pertama-tama kita harus mendapatkan nomor kesalahan terakhir:DIE()
int e = err_last();
Fungsi mengembalikan nomor kesalahan terakhir yang ditetapkan oleh fungsi sistem terakhir yang telah kita panggil (fungsi sistem berikutnya yang kita panggil dapat menimpa nomor kesalahan). Selanjutnya, kami menerjemahkan nomor kesalahan ke teks yang dapat dibaca manusia:
const char *err = err_strptr(e);
Fungsi kita mengembalikan pointer ke buffer yang dialokasikan secara statis yang tidak boleh kita modifikasi. Teks berisi pesan kesalahan, dan kami menunjukkannya kepada pengguna bersama dengan informasi berguna lainnya (nama fungsi, nama file sumber dan baris).
Kesalahan Sistem pada UNIX
Untuk mendapatkan pesan kesalahan terakhir kita hanya mengembalikan nilai variabel global:errno
#include <errno.h>
int err_last()
{
return errno;
}
Dan untuk mendapatkan pesan kesalahan:
#include <string.h>
const char* err_strptr(int code)
{
return strerror(code);
}
Kesalahan Sistem pada Windows
Kami tidak dapat mengakses variabel kesalahan global pada Windows secara langsung. Sebagai gantinya, kita menggunakan fungsi untuk membacanya:
int err_last()
{
return GetLastError();
}
Untuk mendapatkan pesan kesalahan, kita harus mengubahnya menjadi UTF-8:
const char* err_strptr(int code)
{
static char buf[1000];
wchar_t w[250];
unsigned int flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_MAX_WIDTH_MASK;
int n = FormatMessageW(flags, 0, code, 0, w, 250, 0);
if (n == 0) {
buf[0] = '\0';
return buf;
}
WideCharToMultiByte(CP_UTF8, 0, w, -1, buf, sizeof(buf), NULL, NULL);
return buf;
}
Perhatikan bahwa meskipun untuk contoh kita menggunakan buffer baik-baik saja, implementasi ini tidak akan bekerja dengan andal dalam aplikasi multi-threaded, jadi gunakan dengan hati-hati.static
Mengatur Kesalahan Sistem Terakhir
Terkadang kita perlu mengatur atau memodifikasi nomor kesalahan terakhir secara manual. Kita mungkin memerlukan ini, misalnya, ketika meneruskan nomor kesalahan ke fungsi induk. Ketika fungsi turunan memenuhi beberapa kondisi kesalahan, ia dapat memilih untuk menangani kesalahan khusus ini sendiri dan menetapkan nomor kesalahan yang berbeda untuk induknya. Kemudian, ketika fungsi induk melihat nomor kesalahan ini, ia dapat melakukan beberapa operasi tertentu atau mungkin hanya mencetak pesan kesalahan dan melanjutkan pekerjaan normal. Selain itu, kita mungkin ingin menggunakan kode kesalahan khusus aplikasi kita sendiri - mengapa tidak menggunakan variabel global yang sama yang sudah kita miliki secara default?
Ini adalah bagaimana kita mengatur kode kesalahan pada UNIX:
void err_set(int code)
{
errno = code;
}
Dan inilah hal yang sama untuk Windows:
void err_set(int code)
{
SetLastError(code);
}
Kode Kesalahan Sistem Umum
Berikut adalah tabel kecil untuk kode kesalahan sistem yang paling penting, jika Anda perlu menanganinya secara khusus dalam kode Anda:
UNIX Windows Meaning
=================================================================
EINVAL ERROR_INVALID_PARAMETER You've specified an invalid parameter to a function
EBADF ERROR_INVALID_HANDLE You've specified an invalid file descriptor
EACCES ERROR_ACCESS_DENIED You don't have permission to perform the operation
ENOENT ERROR_FILE_NOT_FOUND The file/dir doesn't exist or the path is invalid
ERROR_PATH_NOT_FOUND
ERROR_INVALID_NAME
ERROR_NOT_READY
EEXIST ERROR_FILE_EXISTS The file/dir already exists
ERROR_ALREADY_EXISTS
EAGAIN WSAEWOULDBLOCK The operation can't complete immediately
EWOULDBLOCK
Di UNIX Anda dapat mencantumkan semua nomor kesalahan umum dengan mengeksekusi . Untuk melihat nomor kesalahan apa yang dapat mengembalikan fungsi UNIX tertentu, jalankan dan gulir ke bagian .man errno
man FUNCTION_NAME
ERRORS
Hasil
Kami telah mempelajari cara mendapatkan dan mengatur kode kesalahan sistem dan cara mendapatkan pesan deskripsi kesalahan untuk nomor kesalahan yang diberikan.
Referensi: ffos/error.h
Manajemen File
Dalam bab ini kita akan mempelajari cara:
Membuat file baru
Membuat direktori baru
Dapatkan properti file
Mengatur properti file
ganti nama file
Membuat daftar file dalam direktori
Hapus File
Hapus Direktori
Membuat / mengubah nama / menghapus file dan direktori
Dalam contoh ini kita membuat direktori, membuat file di dalamnya, mengganti nama file, lalu menghapus file dan direktori.
Gulir ke bawah ke . Pertama, kita membuat direktori baru:main()
int r = dir_make("file-man-dir");
assert(r == 0);
Buat file kosong baru:
file f = file_open("file-man-dir/file.tmp", _FILE_CREATE | FILE_WRITE);
assert(f != FILE_NULL);
file_close(f);
Kami menggunakan flag sehingga sistem akan membuat file jika tidak ada; Dan jika file sudah ada, kami akan membukanya. Terkadang berguna saat membuat file gagal jika file ini sudah ada. Misalnya, kami ingin memastikan bahwa kami tidak tiba-tiba menimpa file penting lainnya di komputer pengguna. Saya akan menunjukkan ini dalam contoh berikutnya. berarti kita hanya ingin menulis ke file ini. Jika kita mencoba melakukan operasi membaca, mereka akan gagal. Meskipun dalam contoh ini kita tidak benar-benar menulis data ke file, tetapi tetap saja, untuk dapat membuat file kita harus menggunakan flag, jika tidak OS akan mengembalikan kesalahan._FILE_CREATE
FILE_WRITE
FILE_WRITE
Ubah nama file:
int r = file_rename("file-man-dir/file.tmp", "file-man-dir/newfile.tmp");
assert(r == 0);
Hapus file:
int r = file_remove("file-man-dir/newfile.tmp");
assert(r == 0);
Hapus direktori:
int r = dir_remove("file-man-dir");
assert(r == 0);
Mengembalikan 0 pada kesuksesan lebih baik
Semua fungsi di atas berhasil dikembalikan, kecuali , karena perlu mengembalikan deskriptor file. Mengembalikan kesuksesan dan kesalahan adalah pendekatan yang lebih baik dibandingkan dengan mengembalikan kesuksesan dan kesalahan (yaitu nilai pengembalian boolean). Keuntungan pertama - ini adalah bagaimana sebagian besar fungsi UNIX dirancang. Kedua - kami dapat meningkatkan fungsi kami kapan saja sehingga dapat mengembalikan beberapa kode kesalahan yang berbeda, dan fungsi induk mungkin memerlukan informasi ini. Ketiga - kita bahkan dapat menggunakan kode pengembalian 3-nilai yang kadang-kadang berguna dalam loop di mana kita ingin melanjutkan iterasi atau menghentikan:0
file_open()
0
!=0
1
0
for (;;) {
int r = func();
if (r < 0) {
// error
break;
} else if (r > 0) {
// success
break;
}
// r == 0: keep iterating
}
Pengembalian kesuksesan: kami melanjutkan iterasi
0
Pengembalian (atau nilai apa pun) setelah penyelesaian operasi berhasil: kami keluar dari loop
1
>0
Kembalikan (atau nilai apa pun) karena kesalahan: kami keluar dari loop
-1
<0
Akibatnya, logikanya sederhana: kami melanjutkan iterasi sementara fungsi terus kembali , atau kami memutuskan loop sebaliknya.
0
Membuat / mengubah nama / menghapus file dan direktori di UNIX
Untuk membuat file kita menggunakan flag untuk . Tanpa itu, fungsi akan gagal kecuali file sudah ada. Kami lulus sebagai mode file default untuk file baru, yang berarti bahwa file yang disimpan pada disk akan tersedia untuk dibaca dan ditulis ke setiap akun pengguna di sistem. Namun, jika mode pembuatan file pengguna adalah mis. (dikembalikan oleh pada instalasi Linux saya), mode yang dihasilkan untuk file kami adalah , yang berarti bahwa hanya akun pengguna saya dan pengguna dari grup saya yang dapat menulis ke file ini. Orang lain mungkin hanya membacanya. Izin ini diberlakukan oleh FS. Akhirnya, flag berarti kita ingin membuka file untuk menulis saja.O_CREAT
open()
0666
0002
umask
0664
O_WRONLY
#define _FILE_CREATE O_CREAT
#define FILE_WRITE O_WRONLY
file file_open(const char *name, unsigned int flags)
{
return open(name, flags, 0666);
}
Sisanya cukup mudah:
int file_rename(const char *oldpath, const char *newpath)
{
return rename(oldpath, newpath);
}
int file_remove(const char *name)
{
return unlink(name);
}
int dir_make(const char *name)
{
return mkdir(name, 0777);
}
int dir_remove(const char *name)
{
return rmdir(name);
}
Perhatikan bahwa juga membutuhkan parameter mode file. Kami menggunakan agar semua orang dapat membaca, menulis, masuk ke direktori ini dan daftar isinya. Untuk nilai mode yang dihasilkan untuk direktori kami adalah - pengguna lain hanya dapat membaca konten direktori, tetapi tidak dapat membuat file baru di dalam direktori, misalnya.mkdir()
0777
umask
0002
0775
Buat / ganti nama / hapus file dan direktori di Windows
Untuk membuat file jika tidak ada, kami menggunakan flag. Perhatikan bahwa kami telah meningkatkan fungsi kami dari terakhir kali: sekarang kami membaca 4 bit rendah dari pengguna , dan jika nilainya , kami gunakan untuk membuka hanya file-file yang sudah ada. Ini meniru perilaku UNIX. Misalnya, jika kita menelepon - ini berarti kita hanya ingin membuka file yang ada. Jika kita memanggil - ini berarti kita juga membuat file jika tidak ada.OPEN_ALWAYS
file_open()
flags
0
OPEN_EXISTING
file_open(..., FILE_WRITE)
file_open(..., _FILE_CREATE | FILE_WRITE)
#define _FILE_CREATE OPEN_ALWAYS
#define FILE_WRITE GENERIC_WRITE
file file_open(const char *name, unsigned int flags)
{
wchar_t w[1000];
if (!MultiByteToWideChar(CP_UTF8, 0, name, -1, w, 1000))
return FILE_NULL;
unsigned int creation = flags & 0x0f;
if (creation == 0)
creation = OPEN_EXISTING;
unsigned int access = flags & (GENERIC_READ | GENERIC_WRITE);
return CreateFileW(w, access, 0, NULL, creation, FILE_ATTRIBUTE_NORMAL, NULL);
}
Untuk sisa kode ada pola yang sama: mengkonversi nama file UTF-8 ke UTF-16 dan meneruskan string karakter lebar ke Windows. Perlu dijelaskan hanya bendera untuk . Saat kita mengganti nama file, jika file target sudah ada, biasanya fungsi kembali dengan kesalahan. Tapi bendera ini memaksa Windows untuk diam-diam overwite file target. Ini meniru perilaku UNIX.MOVEFILE_REPLACE_EXISTING
MoveFileExW()
int file_rename(const char *oldpath, const char *newpath)
{
wchar_t w_old[1000];
if (!MultiByteToWideChar(CP_UTF8, 0, oldpath, -1, w_old, 1000))
return -1;
wchar_t w_new[1000];
if (!MultiByteToWideChar(CP_UTF8, 0, newpath, -1, w_new, 1000))
return -1;
return !MoveFileExW(w_old, w_new, MOVEFILE_REPLACE_EXISTING);
}
int file_remove(const char *name)
{
wchar_t w[1000];
if (!MultiByteToWideChar(CP_UTF8, 0, name, -1, w, 1000))
return -1;
return !DeleteFileW(w);
}
int dir_make(const char *name)
{
wchar_t w[1000];
if (!MultiByteToWideChar(CP_UTF8, 0, name, -1, w, 1000))
return -1;
return !CreateDirectoryW(w, NULL);
}
int dir_remove(const char *name)
{
wchar_t w[1000];
if (!MultiByteToWideChar(CP_UTF8, 0, name, -1, w, 1000))
return -1;
return !RemoveDirectoryW(w);
}
Hasil
Bagus! Kami telah belajar bagaimana melakukan operasi file/direktori dasar.
Referensi: ffos/file.h, ffos/dir.h
Properti File
Dalam contoh ini kita membuat file baru, mendapatkan meta datanya kemudian memperbarui waktu dan atribut modifikasi file.
Gulir ke bawah ke fungsi. Kami membuat file baru di sini, tetapi tidak seperti pada contoh sebelumnya, kami tidak ingin menimpanya jika sudah ada. Jika file sudah ada, OS akan mengembalikan kesalahan kepada kami. Kami memaksakan perilaku ini dengan menggunakan bendera.main()
FILE_CREATENEW
file f = file_open("file-props.tmp", FILE_CREATENEW | FILE_WRITE);
Sekarang dapatkan properti file: ukuran, waktu modifikasi, atribut. Kita membutuhkan struktur kita sendiri untuk memegang properti ini - . Kami meneruskan objek ini ke kami yang akan mengisinya dengan data dan laba atas kesuksesan.fileinfo
file_info()
0
fileinfo fi = {};
assert(0 == file_info(f, &fi));
Kemudian kita menggunakan fungsi untuk mendapatkan properti. Ingat bahwa kita tidak boleh mengakses bidangnya secara langsung (untuk mempertahankan antarmuka lintas platform), jadi kita perlu mendapatkan properti file menggunakan fungsi pembantu kecil. Untuk mendapatkan ukuran file:fileinfo_*()
unsigned long long file_size = fileinfo_size(&fi);
Untuk dapat bekerja dengan timestamp kita juga membutuhkan objek cross-platform kita sendiri, sebut saja . Ini memiliki 2 bidang terpisah: satu untuk jumlah detik sejak tahun 1 dan yang lainnya adalah angka nanodetik.datetime
typedef struct {
long long sec;
unsigned int nsec;
} datetime;
Inilah cara kami mendapatkan waktu modifikasi terakhir file:
datetime t = fileinfo_mtime(&fi);
Atribut (atau mode file):
unsigned int attr = fileinfo_attr(&fi);
Untuk memeriksa apakah file tersebut adalah direktori atau bukan:
unsigned int its_a_directory = file_isdir(attr);
Untuk memperbarui waktu modifikasi file terakhir:
datetime t = ...;
assert(0 == file_set_mtime(f, t));
Atribut file pada UNIX dan Windows adalah dua hal yang sama sekali berbeda. Itu sebabnya nomor atribut aktual harus diatur di dalam cabang preprocessor. Kami mengatur mode pada UNIX yang berarti bahwa kami membatasi akses ke file ini untuk semua orang kecuali kami (akun pengguna kami). Dan kami mengatur bendera di Windows. Saya ulangi: ini bukan hal yang sama, ini hanya misalnya.0600
read-only
unsigned int attr = ...;
assert(0 == file_set_attr(f, attr));
Properti file di UNIX
Ini adalah bagaimana kita mendefinisikan bendera kita pada UNIX:FILE_CREATENEW
#define FILE_CREATENEW (O_CREAT | O_EXCL)
Kami adalah alias untuk . Dan adalah fungsi yang mengisinya dengan data untuk jalur file yang ditentukan.fileinfo
struct stat
fstat()
#include <sys/stat.h>
typedef struct stat fileinfo;
int file_info(file f, fileinfo *fi)
{
return fstat(f, fi);
}
Untuk mendapatkan ukuran dan atribut file (mode):
unsigned long long fileinfo_size(const fileinfo *fi)
{
return fi->st_size;
}
unsigned int fileinfo_attr(const fileinfo *fi)
{
return fi->st_mode;
}
int file_isdir(unsigned int file_attr)
{
return ((file_attr & S_IFMT) == S_IFDIR);
}
Mendapatkan waktu modifikasi file sedikit rumit, karena di macOS bidangnya memiliki nama yang berbeda dari di Linux.
#define TIME_1970_SECONDS 62135596800ULL
datetime datetime_from_timespec(struct timespec ts)
{
datetime t = {
.sec = TIME_1970_SECONDS + ts.tv_sec,
.nsec = (unsigned int)ts.tv_nsec,
};
return t;
}
datetime fileinfo_mtime(const fileinfo *fi)
{
#if defined __APPLE__ && defined __MACH__
return datetime_from_timespec(fi->st_mtimespec);
#else
return datetime_from_timespec(fi->st_mtim);
#endif
}
Di sini kita juga menggunakan fungsi helper untuk mengkonversi antara representasi waktu UNIX dan kita sendiri. Stempel waktu UNIX adalah jumlah detik yang berlalu sejak 1 Januari 1970, dengan bidang terpisah untuk angka nanodetik. Kami mengonversi ini ke stempel waktu kami yang merupakan jumlah detik sejak tahun 1. Angka nanodetik sama untuk kita.datetime_from_timespec()
Mengatur waktu modifikasi untuk deskriptor file memerlukan fungsi konversi waktu terbalik (dari tahun 1 hingga tahun 1970):
struct timeval datetime_to_timeval(datetime t)
{
struct timeval tv = {
.tv_sec = t.sec - TIME_1970_SECONDS,
.tv_usec = t.nsec / 1000,
};
return tv;
}
int file_set_mtime(file f, datetime last_write)
{
struct timeval tv[2];
tv[0] = datetime_to_timeval(last_write);
tv[1] = datetime_to_timeval(last_write);
return futimes(f, tv);
}
futimes()
Membutuhkan 2 nilai stempel waktu: array dari 2 objek timeval, dengan yang pertama adalah waktu akses file, dan yang kedua - waktu modifikasi file. Di sini kami hanya memperbarui keduanya secara bersamaan. Tetapi jika Anda ingin membiarkan waktu akses apa adanya, Anda mungkin perlu membuat fungsi baru, mis.file_set_amtime(file f, datetime access, datetime last_write)
Terakhir, atur atribut file:
int file_set_attr(file f, unsigned int mode)
{
return fchmod(f, mode);
}
Anda dapat melihat semua kemungkinan nilai yang mendukung dengan mengeksekusi pada instalasi UNIX Anda.fchmod()
man fchmod
Properti file di Windows
Untuk membuat file baru di Windows kami menggunakan bendera:CREATE_NEW
#define FILE_CREATENEW CREATE_NEW
Untuk mendapatkan properti file dengan deskriptor file:
typedef BY_HANDLE_FILE_INFORMATION fileinfo;
int file_info(file f, fileinfo *fi)
{
return !GetFileInformationByHandle(f, fi);
}
Mendapatkan ukuran file dengan menggabungkan dua nilai 32-bit bersama-sama:
unsigned long long fileinfo_size(const fileinfo *fi)
{
return ((unsigned long long)fi->nFileSizeHigh << 32) | fi->nFileSizeLow;
}
Mendapatkan atribut file dan memeriksa apakah itu direktori:
unsigned int fileinfo_attr(const fileinfo *fi)
{
return fi->dwFileAttributes;
}
int file_isdir(unsigned int file_attr)
{
return ((file_attr & FILE_ATTRIBUTE_DIRECTORY) != 0);
}
Mengatur atribut file:
int file_set_attr(file f, unsigned int attr)
{
FILE_BASIC_INFO i = {};
i.FileAttributes = attr;
return !SetFileInformationByHandle(f, FileBasicInfo, &i, sizeof(FILE_BASIC_INFO));
}
Mendapatkan waktu modifikasi terakhir file agak rumit, karena stempel waktu internal adalah interval 100-nanodetik sejak tahun 1600. Kita harus mengonversi format ini ke format .datetime
#define TIME_100NS 116444736000000000ULL // 100-ns intervals within 1600..1970
datetime datetime_from_filetime(FILETIME ft)
{
datetime t = {};
unsigned long long i = ((unsigned long long)ft.dwHighDateTime << 32) | ft.dwLowDateTime;
if (i > TIME_100NS) {
i -= TIME_100NS;
t.sec = TIME_1970_SECONDS + i / (1000000 * 10);
t.nsec = (i % (1000000 * 10)) * 100;
}
return t;
}
datetime fileinfo_mtime(const fileinfo *fi)
{
return datetime_from_winftime(fi->ftLastWriteTime);
}
Mengatur waktu modifikasi terakhir adalah tindakan sebaliknya:
FILETIME datetime_to_filetime(datetime t)
{
t.sec -= TIME_1970_SECONDS;
unsigned long long d = t.sec * 1000000 * 10 + t.nsec / 100 + TIME_100NS;
FILETIME ft = {
.dwLowDateTime = (unsigned int)d,
.dwHighDateTime = (unsigned int)(d >> 32),
};
return ft;
}
int file_set_mtime(file f, datetime last_write)
{
FILETIME ft = datetime_to_filetime(last_write);
return !SetFileTime(f, NULL, &ft, &ft);
}
Parameter ke-3 dan ke-4 adalah waktu akses file baru dan waktu modifikasi terakhir. Jika kita tidak ingin mengubah waktu akses, kita bisa mengatur parameter ke (tapi ini akan salah karena UNIX tidak mendukung perilaku seperti itu).SetFileTime()
NULL
file_set_mtime()
Hasil
Kami telah belajar cara mendapatkan properti file dan memperbarui beberapa di antaranya.
Referensi: ffos/file.h
Daftar Direktori
Dalam contoh ini kita membuka direktori saat ini untuk daftar isinya dan mencetak semua file / direktori yang dikandungnya.
Gulir ke bawah ke . Untuk membuka direktori kita menggunakan objek struktur tipe . Objek menyimpan beberapa data yang diperlukan untuk daftar direktori. Anggap saja sebagai deskriptor daftar direktori kami sendiri. Parameter kedua adalah jalur direktori (absolut atau relatif). Fungsi kembali pada kesuksesan.main()
dirscan
0
dirscan ds = {};
assert(0 == dirscan_open(&ds, "."));
Pendekatan desain yang berbeda
Kita bisa saja mendesain untuk mengembalikan salinan objek, tetapi dalam kasus ini kompiler dapat menghasilkan kode yang tidak efektif karena salinan data objek harus dilakukan, dan pointer di dalamnya mungkin menjadi tidak valid:dirscan_open()
dirscan
dirscan
// BAD (data copy; inconvenient to return an error)
dirscan dirscan_open(const char *path) { ... }
Kami juga dapat mengalokasikan pointer secara dinamis di dalam fungsi dan mengembalikannya, tetapi dalam kasus ini kami tidak mengizinkan pengguna untuk memutuskan di wilayah memori mana data harus disimpan:dirscan*
// BAD (user can't decide what memory region to use)
dirscan* dirscan_open(const char *path) { ... }
Jadi pendekatan yang saya pilih adalah objek pengguna sebagai parameter, karena tidak ada salinan data, dan ini adalah solusi fleksibel untuk manajemen memori:
int dirscan_open(dirscan *d, const char *path) { ... }
Satu-satunya persyaratan adalah ketika menggunakan pola ini, kita perlu menghapus wilayah memori dengan memusatkan perhatian terlebih dahulu. melakukan ini secara otomatis untuk kita pada saat pembuatan objek. Namun, jika kita ingin menggunakannya kembali nanti, maka kita perlu menghapus datanya secara manual dengan (baik setelah masing-masing atau sebelum masing-masing):dirscan d = {};
memset()
dirscan_close()
dirscan_open()
// use the `d` object for the first time
dirscan d = {};
dirscan_open(&d, ...);
dirscan_close(&d);
// We need to reset the data inside `d` before passing it to dirscan_open()
// Inconvenient, but can be easily replaced with MEM_ZERO_OBJ(&d) macro
memset(&d, 0, sizeof(d));
// use the same `d` object again
dirscan_open(&d, ...);
dirscan_close(&d);
Dengan cara ini kami menghindari potensi masalah dan kebocoran informasi yang tidak diinginkan. Dalam kode perpustakaan saya, saya biasanya berasumsi bahwa objek input sudah disiapkan dengan cara ini, jika tidak, saya harus menggunakan di awal setiap fungsi seperti . Tapi itu tidak akan selalu berhasil, karena kadang-kadang saya ingin menyiapkan beberapa bidang di objek saya terlebih dahulu, sebelum meneruskannya ke fungsi:memset()
dirscan_open()
typedef struct {
int some_option;
} dirscan;
int dirscan_open(dirscan *d, const char *path)
{
memset(d, 0, sizeof(*d)); // INCORRECT (user can't configure the input dirscan object)
...
}
void main()
{
// use the `d` object without initializing it with 0
dirscan d;
d.some_option = 1; // DOESN'T WORK because the setting will be reset inside `dirscan_open()`
dirscan_open(&d, ...);
...
}
Kita bisa memecahkan masalah di atas dengan menggunakan struktur konfigurasi terpisah:
struct dirscan_conf {
int some_option;
};
int dirscan_open(dirscan *d, const struct dirscan_conf *conf, const char *path)
{
memset(d, 0, sizeof(*d));
...
}
void main()
{
// Note: `d = {}` is needed anyway,
// otherwise it will contain garbage until we call dirscan_open()
dirscan d = {};
struct dirscan_conf dconf = {
.some_option = 1,
};
dirscan_open(&d, &dconf, ...);
...
}
Meskipun ini adalah solusi yang baik, saya tidak suka memiliki 2 struktur, bukan hanya 1. Saya juga tidak ingin objek saya mengandung sampah (saya bisa salah mengakses datanya dan kemudian mengalami kesulitan selama debugging). Karena itu, masih menjadi persyaratan. Tapi bagaimanapun, saya baru saja menunjukkan kepada Anda beberapa pendekatan tentang cara mendesain fungsi - Anda memilih yang terbaik untuk kasus penggunaan Anda, jangan dengarkan siapa pun.dirscan d = {};
Daftar Direktori (lanjutan)
Mari kita kembali ke kode contoh kita. Langkah selanjutnya adalah membaca nama file dari direktori satu per satu dan mencetaknya ke stdout:
const char *name;
while (NULL != (name = dirscan_next(&ds))) {
puts(name);
}
Setelah pengembalian kami yang berarti bahwa itu telah berhasil menyelesaikan traversal direktori atau ada kesalahan selama proses, kami memeriksa kasus mana itu dengan membandingkan kesalahan sistem terakhir dengan kode kesalahan khusus kami:dirscan_next()
NULL
ERR_NOMOREFILES
assert(err_last() == ERR_NOMOREFILES);
Terakhir, tutup deskriptor kami:
dirscan_close(&ds);
// memset(&d, 0, sizeof(d)); // we may want to reset it here
Anda dapat memilih untuk menghapus data di dalam tepat setelah menutupnya, sehingga menghilangkan persyaratan untuk melakukannya sebelum menggunakan kembali objek dengan lagi.ds
dirscan_open()
Daftar Direktori di UNIX
Pertama kita perlu mendeklarasikan struktur C kita sendiri untuk memegang deskriptor direktori:
#include <dirent.h>
typedef struct {
DIR *dir;
} dirscan;
Implementasi kami mengembalikan kesuksesan dan menyimpan pointer di dalam objek kami:dirscan_open()
0
DIR*
int dirscan_open(dirscan *d, const char *path)
{
DIR *dir = opendir(path);
if (dir == NULL)
return -1;
d->dir = dir;
return 0;
}
Ketika kami menelepon, kami mengharapkannya untuk kembali untuk menunjukkan bahwa traversal (yaitu loop dalam kode pengguna) harus dihentikan. fungsi akan diperbarui jika gagal karena kesalahan. Jika tidak, akan dibiarkan tidak berubah, yang dalam kasus kami - ini berarti bahwa tidak ada lagi entri untuk kembali. berisi beberapa bidang, tetapi kami hanya tertarik pada satu - yang merupakan nama file yang dihentikan NULL (tanpa jalur). Data teks aktual untuk nama file dialokasikan di dalam libc, dan kita tidak boleh menggunakannya setelah menutup deskriptor direktori kita.dirscan_next()
NULL
readdir()
errno
errno
0
struct dirent
d_name
#define ERR_NOMOREFILES 0
const char* dirscan_next(dirscan *d)
{
const struct dirent *de;
errno = ERR_NOMOREFILES;
if (NULL == (de = readdir(d->dir)))
return NULL;
return de->d_name;
}
Tutup deskriptor di mana titik libc membebaskan buffer internalnya yang dialokasikan untuk kita:
void dirscan_close(dirscan *d)
{
closedir(d->dir);
d->dir = NULL;
}
Perhatikan bagaimana kita mengatur ulang pointer setelah menutupnya. Ini diperlukan karena biasanya fungsi dekat harus berfungsi dengan benar jika pengguna memanggilnya beberapa kali. Dalam contoh kita, jika pengguna memanggil fungsi kita lebih dari sekali, tidak ada yang akan rusak. Jika tidak, pada panggilan berikutnya kami akan mencoba membebaskan pointer yang sama dua kali yang dapat mengakibatkan crash.NULL
DIR*
Satu hal lagi tentang pola penggunaan objek struktur. Kami mungkin ingin melindungi diri dari penggunaan fungsi kami yang tidak valid, seperti ketika pengguna memanggil fungsi dengan penunjuk atau objek yang buruk. Anda dapat memasukkan ke dalam setiap fungsi sehingga mereka akan mencetak pesan kesalahan sebelum crash:NULL
assert(d != NULL);
int dirscan_open(dirscan *d, const char *path)
{
assert(d != NULL);
...
}
Namun, itu tidak akan membantu ketika pengguna memanggil kami dengan penunjuk sampah. Jadi pada akhirnya saya pikir ini menegaskan di mana-mana dalam kode kami tidak akan sangat membantu, tetapi Anda harus melakukan apa yang menurut Anda tepat untuk Anda.
Daftar Direktori di Windows
Ini adalah bagaimana kami mendeklarasikan struktur kami untuk daftar direktori:
typedef struct {
HANDLE dir;
WIN32_FIND_DATAW data;
char name[260 * 4];
unsigned next;
} dirscan;
dir
adalah deskriptor sistem daftar direktori. adalah objek yang diisi Windows untuk kita dengan informasi tentang setiap file/direktori. Buffer kita harus menyimpan nama file dari tetapi dalam pengkodean UTF-8.data
name
WIN32_FIND_DATAW
Fungsi pembukaan secara logis dibagi menjadi 2 langkah:
Siapkan string karakter lebar yang menahan jalur direktori dengan di akhir. Dengan demikian, kami memberi tahu Windows untuk menyertakan semua file dalam daftar (wildcard mask).
\*
*
wchar_t w[1000];
int r = MultiByteToWideChar(CP_UTF8, 0, path, -1, w, 1000 - 2);
if (r == 0)
return -1;
r--;
w[r++] = '\\';
w[r++] = '*';
w[r] = '\0';
Ingat bahwa , ketika dipanggil dengan ukuran teks input, mengembalikan jumlah karakter lebar termasuk NULL terakhir, itu sebabnya kita perlu setelahnya.MultiByteToWideChar()
-1
r--
Panggilan untuk membuka daftar direktori. Fungsi mengisi kami dengan informasi untuk entri pertama.
FindFirstFileW()
d->data
HANDLE dir = FindFirstFileW(w, &d->data);
if (dir == INVALID_HANDLE_VALUE && GetLastError() != ERROR_FILE_NOT_FOUND)
return -1;
d->dir = dir;
return 0;
FindFirstFileW()
kembali dengan kesalahan jika tidak ada file di dalam direktori. Kita tidak boleh gagal di sini dalam fungsi kita untuk meniru perilaku UNIX, jadi kita menangani kasus ini.ERROR_FILE_NOT_FOUND
OK, kami telah membuka direktori dan sekarang kami sudah memiliki entri pertama yang siap dikembalikan ke pengguna. Kami memasukkan cabang ini, yang memeriksa kasus kesalahan di atas dengan , dan kembali ke pengguna. Kami mengatur bendera sehingga kami tidak akan memasuki cabang ini di lain waktu.ERROR_FILE_NOT_FOUND
NULL
if (!d->next) {
if (d->dir == INVALID_HANDLE_VALUE) {
SetLastError(ERROR_NO_MORE_FILES);
return NULL;
}
d->next = 1;
}
Sekarang kita hanya mengkonversi nama file menjadi UTF-8 dan mengembalikannya ke pengguna.
if (0 == WideCharToMultiByte(CP_UTF8, 0, d->data.cFileName, -1, d->name, sizeof(d->name), NULL, NULL))
return NULL;
return d->name;
Lain kali pengguna memanggil fungsi kita, kita memasuki cabang kedua yang memanggil sehingga Windows mengisi objek kita dengan informasi untuk entri berikutnya. Ketika tidak ada lagi entri, ia kembali dan menetapkan kode kesalahan. Setelah itu kami harapkan dari pengguna yang akan dipanggil di beberapa titik.FindNextFileW()
data
0
ERROR_NO_MORE_FILES
FindClose()
if (!d->next) {
...
} else {
if (!FindNextFileW(d->dir, &d->data))
return NULL;
}
Hasil
Kami telah belajar cara membuat daftar semua file di dalam direktori.
Referensi: ffos/dirscan.h
Pipa Tanpa Nama
Ingat ketika kita berbicara tentang pengalihan deskriptor I / O standar? Proses pengalihan sebenarnya diimplementasikan melalui pipa. Pipa adalah objek yang dapat kita gunakan untuk membaca data dari proses lain, atau menulis data ke dalamnya. Tentu saja kita belum tahu tentang proses sistem, jadi kita hanya menggunakan kedua ujung pipa sendiri dalam contoh berikutnya.
Gulir ke bawah ke . Pertama, kita perlu membuat pipa baru dan mendapatkan deskriptornya. Setiap pipa memiliki dua ujung: yang kita (atau proses lain) baca dari dan yang kita (atau proses lain) menulis ke. Fungsi kami mengembalikan kesuksesan dan menetapkan deskriptor baca &; tulis ke variabel kami.main()
0
pipe_t r, w;
assert(0 == pipe_create(&r, &w));
Pipa kami sudah siap dan kami melanjutkan dengan menulis beberapa data ke dalamnya. Fungsi penulisan, seperti biasa, mengembalikan jumlah byte yang ditulis atau kesalahan. Ketika kita menulis ke pipa, kita harus memahami bahwa data kita tidak menjadi ajaib terlihat oleh proses lain. OS menyalin data kami ke buffer internal (ukuran terbatas) dan kemudian fungsi kembali kepada kami. Kita dapat memanggil fungsi penulisan beberapa kali dan akan selalu segera kembali, kecuali buffer internal benar-benar penuh. Kemudian, ketika proses lain membaca dari pipa, data dari buffer internal disalin ke buffer pembaca dan kursor digeser ke depan sesuai. Jika pembaca tidak membaca data dari pipa, tetapi penulis ingin menulis lebih banyak, pada suatu saat fungsi penulisan akan memblokir (tidak akan kembali) sampai buffer internal memiliki beberapa ruang kosong.-1
ssize_t n = pipe_write(w, "hello!", 6);
assert(n >= 0);
Mari kita asumsikan bahwa kita tidak ingin menulis data lagi ke pipa, jadi kita hanya menutup deskriptor tulis karena kita tidak membutuhkannya lagi.
pipe_close(w);
Sekarang kita membaca beberapa data dari pipa, menggunakan deskriptor baca. Ingat bahwa dalam program nyata kita tidak tahu berapa byte fungsi akan kembali kepada kita, jadi kita biasanya perlu melakukan pembacaan dalam satu lingkaran. Juga, ketika tidak ada lagi data untuk dibaca saat ini, fungsi akan memblokir proses kita (fungsi tidak akan kembali) sampai beberapa data tersedia untuk dibaca atau sinyal sistem diterima. Tetapi untuk contoh sederhana ini kita tidak peduli tentang itu. Kami belum siap untuk menulis kode yang sempurna, kami hanya mempelajari fungsinya, jadi untuk saat ini ingatlah tentang masalah tersebut.
char buf[100];
ssize_t n = pipe_read(r, buf, sizeof(buf));
assert(n >= 0);
Dalam kasus kami di sini, fungsi sebenarnya akan mengembalikan jumlah byte yang sama dengan yang telah kami tulis sebelumnya, dan buffer kami akan berisi data yang sama dengan yang kami tulis (karena kami telah menulis lebih sedikit byte daripada kapasitas buffer bacaan). Tapi mari kita coba membaca beberapa byte lagi.
n = pipe_read(r, buf, sizeof(buf));
assert(n == 0);
Sekarang fungsinya akan kembali kepada kita, yang berarti tidak ada lagi data di dalam pipa, dan tidak akan ada lagi data untuk deskriptor ini. Ini karena kami telah menutup deskriptor tulis sebelumnya. OS mengingat itu dan meneruskan pengetahuan ini ke proses yang membaca dari pipa.0
Pipa tanpa nama di UNIX
Sebuah deskriptor pipa di UNIX juga merupakan bilangan bulat, sama seperti deskriptor file. Kami menggunakan fungsi I / O yang sama untuk pipa yang kami gunakan sebelumnya. Hanya perlu dikatakan bahwa fungsi sistem sebenarnya mengambil satu argumen yang merupakan array dari dua bilangan bulat. Saya tidak suka desain ini, karena saya lupa mana yang read-deskriptor dan mana yang write-descriptor. Jadi saya menggunakan nilai terpisah.pipe()
typedef int pipe_t;
#define FFPIPE_NULL (-1)
int pipe_create(pipe_t *rd, pipe_t *wr)
{
pipe_t p[2];
if (0 != pipe(p))
return -1;
*rd = p[0];
*wr = p[1];
return 0;
}
void pipe_close(pipe_t p)
{
close(p);
}
ssize_t pipe_read(pipe_t p, void *buf, size_t size)
{
return read(p, buf, size);
}
ssize_t pipe_write(pipe_t p, const void *buf, size_t size)
{
return write(p, buf, size);
}
Pipa Tanpa Nama di Windows
Deskriptor pipa memiliki jenis yang sama dengan deskriptor file. Fungsi I / O mirip dengan file dengan satu pengecualian: kita harus meniru perilaku UNIX dalam fungsi membaca kita dan kembali ketika deskriptor tulis pipa ditutup. Dalam hal ini kembali dengan kode kesalahan.0
ReadFile()
ERROR_BROKEN_PIPE
typedef HANDLE pipe_t;
#define FFPIPE_NULL INVALID_HANDLE_VALUE
int pipe_create(pipe_t *rd, pipe_t *wr)
{
return !CreatePipe(rd, wr, NULL, 0);
}
void pipe_close(pipe_t p)
{
CloseHandle(p);
}
ssize_t pipe_read(pipe_t p, void *buf, size_t cap)
{
DWORD rd;
if (!ReadFile(p, buf, cap, &rd, 0)) {
if (GetLastError() == ERROR_BROKEN_PIPE)
return 0;
return -1;
}
return rd;
}
ssize_t pipe_write(pipe_t p, const void *data, size_t size)
{
DWORD wr;
if (!WriteFile(p, data, size, &wr, 0))
return -1;
return wr;
}
Hasil
Kami telah belajar cara membuat pipa, membaca dan menulis dari / ke mereka.
Referensi: ffos/pipe.h
Menjalankan Program Lain
Saya pikir ini akan menjadi hal yang cukup menarik bagi Anda - kita akan belajar bagaimana menjalankan program lain. Ketika kita menjalankan proses baru, biasanya dikatakan bahwa kita menjadi proses induk untuk itu dan proses baru adalah proses anak bagi kita. Dalam contoh berikutnya kita akan mengeksekusi file biner kita.dir-list
Gulir ke bawah ke . Karena kita mengacu pada aplikasi yang berbeda, kita menggunakan cabang preprocessor untuk mengatur jalur file yang dapat dieksekusi () dan argumen baris perintah pertama yang akan terlihat seperti dalam proses yang baru dibuat.main()
path
arg0
argv[0]
const char *path = ..., *arg0 = ...;
const char *args[] = {
arg0,
NULL,
};
ps p = ps_exec(path, args);
assert(p != PS_NULL);
Argumen pertama adalah path file lengkap ke file yang dapat dieksekusi. OS akan membuka file ini, perfom proses pemuatan file biner dan kemudian menjalankannya untuk kita. Argumen kedua adalah array argumen baris perintah untuk proses baru. Elemen pertama selalu nama file yang dapat dieksekusi, dan elemen terakhir harus selalu . Fungsi mengembalikan deskriptor proses atau kesalahan.NULL
PS_NULL
Setelah proses baru dibuat, kita dapat menggunakan deskriptor ini untuk mengirim sinyal ke sana atau menunggu sampai selesai. Dalam contoh kami di sini, kami sebenarnya tidak peduli tentang itu, jadi kami hanya menutup deskriptor proses untuk menghindari kebocoran memori:
ps_close(p);
Menjalankan Program Lain di UNIX
Pertama, kami mendeklarasikan variabel global eksternal untuk lingkungan proses kami:
extern char **environ;
Ini adalah array string, setiap pasangan diakhiri dengan byte. Penunjuk string terakhir juga . Misalnya:key=value
NULL
NULL
key1=value1 \0
key2=value2 \0
\0
Secara default ada banyak variabel lingkungan yang ditetapkan untuk setiap proses yang dijalankan dalam sistem dan beberapa program bergantung pada mereka selama pekerjaan mereka. Jadi hal pertama yang harus kita pahami ketika mencoba menjalankan program lain - kita harus melewati (biasanya) set variabel lingkungan yang sama dengan proses kita yang telah dijalankan. Jika tidak, beberapa program mungkin tidak berperilaku dengan benar.
Dan ini adalah bagaimana kita membuat proses baru pada UNIX:
typedef int ps;
#define PS_NULL (-1)
ps ps_exec(const char *filename, const char **argv)
{
pid_t p = vfork();
if (p != 0)
return p;
execve(filename, (char**)argv, environ);
_exit(255);
return 0;
}
Konsep mengeksekusi program lain melalui proses forking:
Parent process OS Child process
===============================================================
...
Calling vfork() -->
[frozen] OS creates and
executes new process -->
vfork() returns 0
execve(...)
[unfrozen]
vfork() returns PID:
pid = vfork()
Fungsi pertama, , membuat salinan cepat dari proses yang sedang berjalan dan mengembalikan nomor ID proses (PID) baru kembali kepada kami. Di sisi lain, fungsi kembali untuk proses kloning kita. Programmer menulis 2 cabang kode menggunakan nilai yang dikembalikan ini seperti yang saya tunjukkan pada kode di atas. Dengan kata lain, setelah dipanggil dalam proses induk, itu akan terkunci (membeku dalam waktu) sampai proses anak membukanya. Pada saat yang sama proses anak mengambil alih eksekusi dengan kembali ke sana.vfork()
0
vfork()
vfork()
0
Sekarang kita berada di dalam proses anak dan kita memanggil untuk meneruskan parameter untuk file baru yang dapat dieksekusi: jalur file, argumen baris perintah, dan variabel lingkungan. Biasanya, fungsi ini tidak kembali ke pemanggil karena executable baru dimuat ke dalam memori dan mengambil alih kontrol eksekusi, tidak ada yang bisa kita lakukan lagi. Namun, jika terjadi kesalahan, kami menelepon untuk keluar dari proses anak kami dengan kesalahan. Ketika sistem telah mentransfer kontrol proses anak kami ke executable lain, proses induk kami, membeku dalam waktu dan menunggu untuk kembali, akhirnya bangun, menerima PID anak dan melanjutkan pekerjaannya. Jelas, PID tidak akan pernah bisa .execve()
_exit()
vfork()
0
Sebenarnya tidak diperlukan untuk menutup deskriptor proses (PID) yang dikembalikan oleh UNIX, tetapi kita masih perlu melakukannya untuk Windows, jadi kita hanya menggunakan fungsi kosong untuk itu di sini:vfork()
void ps_close(ps p)
{
(void)p;
}
(void)p
Notasi diperlukan untuk menekan peringatan kompiler tentang .unused parameter
Menjalankan Program Lain di Windows
Pertama, kita membuat alias untuk jenis deskriptor proses dan mendeklarasikan nilai konstanta yang menunjukkan deskriptor yang tidak valid:
typedef HANDLE ps;
#define _PS_NULL INVALID_HANDLE_VALUE
Dalam kita harus mengkonversi ke UTF-16 tidak hanya path file ke file yang dapat dieksekusi, tetapi juga semua argumen baris perintah. Selain itu, Windows menerima argumen baris perintah bukan sebagai array tetapi sebagai string tunggal. Jadi kita harus mengubah array menjadi string. Setelah persiapan selesai, kami dapat melanjutkan dengan menghubungi :ps_exec()
CreateProcessW()
STARTUPINFOW si = {};
si.cb = sizeof(STARTUPINFO);
PROCESS_INFORMATION info;
if (!CreateProcessW(wfn, wargs, NULL, NULL, 0
, 0, NULL, NULL, &si, &info))
return _PS_NULL;
Argumen ke-7 adalah penunjuk ke array variabel lingkungan. Kami menggunakan sehingga proses baru akan secara otomatis mewarisi lingkungan proses kami. Argumen ke-9 adalah objek yang memungkinkan kita untuk menentukan beberapa opsi tambahan. Kami tidak membutuhkannya sekarang, jadi kami hanya menginisialisasi ukurannya ( bidang) dan itu saja. Parameter terakhir adalah objek di mana fungsi menetapkan deskriptor proses baru untuk kita - - yang kita kembalikan ke pengguna.NULL
STARTUPINFOW
cb
PROCESS_INFORMATION
hProcess
CloseHandle(info.hThread);
return info.hProcess;
Fungsi ini juga menetapkan deskriptor bidang - utas untuk proses baru. Tapi kami tidak membutuhkannya, jadi untuk menghindari kebocoran memori, kami hanya menutupnya.hThread
Hasil
Kami telah belajar cara mengeksekusi file biner asli. Tentu saja, Anda dapat mengubah jalur file di program kecil kami di sini dan menambahkan / memodifikasi argumen baris perintah. Selama Anda tidak menggunakan string yang sangat besar, semuanya akan berfungsi seperti yang diharapkan.
Referensi: ffos/process.h
Menjalankan Program Lain dan Membaca Outputnya
Sekarang untuk contoh terakhir dalam tutorial Level 1, saya ingin menunjukkan setidaknya sesuatu yang keren. Mari kita tingkatkan contoh kita sebelumnya sehingga kita membuat proses baru dan membaca hasilnya sendiri, tetapi biarkan berinteraksi dengan konsol pengguna secara langsung. Kali ini kita akan mengeksekusi file biner kita. Setelah kami membaca beberapa data dari proses anak, kami dapat melakukan apa pun yang kami inginkan dengannya, tetapi di sini kami hanya mencetak data ke stdout.std-echo
Gulir ke . Pertama, kami membuat pipa yang akan bertindak sebagai jembatan antara proses kami dan proses anak. Kita sudah tahu cara kerjanya.main()
pipe_t r, w;
assert(0 == pipe_create(&r, &w));
Sekarang kita membuat proses baru yang akan menggunakan pipa kita untuk stdout / stderr. Kami tidak dapat menggunakan fungsi kami sebelumnya, karena tidak memiliki antarmuka yang tepat. Jadi kita membuat fungsi baru dengan parameter baru yang kita gunakan untuk menentukan properti input. Dalam kasus kami, ini adalah deskriptor penulisan pipa kami. Kami mengatur karena kami ingin proses anak mewarisi konsol stdin kami - fungsi kami akan secara khusus menangani kasus ini.ps_exec()
.in = PIPE_NULL
typedef struct {
const char **argv;
file in, out, err;
} ps_execinfo;
...
const char *args[] = {
arg0,
NULL,
};
ps_execinfo info = {
args,
.in = PIPE_NULL,
.out = w,
.err = w,
};
ps p = ps_exec_info(path, &info);
assert(p != _PS_NULL);
Sekarang proses anak sedang berjalan atau sudah keluar, kita dapat membaca beberapa data dari pipa kita dan meneruskannya apa adanya ke stdout kita. Ingat bahwa mungkin menggantung di dalam kecuali proses anak menulis sesuatu ke stdout atau stderr-nya.pipe_read()
char buf[1000];
ssize_t n = pipe_read(r, buf, sizeof(buf));
assert(n >= 0);
stdout_write(buf, n);
Perhatikan bahwa jika Anda menghapus baris dengan , tidak ada yang akan dicetak, karena, tidak seperti pada contoh sebelumnya, proses anak tidak lagi memiliki akses langsung ke output konsol.stdout_write()
Mengeksekusi Program Lain dan Membaca Output Mereka di UNIX
Fungsi yang ditingkatkan terlihat seperti ini:ps_exec_info()
ps ps_exec_info(const char *filename, ps_execinfo *info)
{
pid_t p = vfork();
if (p != 0)
return p;
if (info->in != -1)
dup2(info->in, 0);
if (info->out != -1)
dup2(info->out, 1);
if (info->err != -1)
dup2(info->err, 2);
execve(filename, (char**)info->argv, environ);
_exit(255);
return 0;
}
Setelah proses anak mengambil kendali, perlu membuat beberapa persiapan sebelum memanggil , karena kita perlu mengatur pengalihan deskriptor standar jika kode induk telah memerintahkan untuk melakukannya. Dalam kasus kami, fungsi induk menyediakan deskriptor pipa yang sama untuk stdout dan stderr - ini berarti bahwa ketika anak menulis ke stdout / stderr, data sebenarnya akan ditulis ke buffer internal pipa kami. Seperti yang sudah Anda ketahui, pada sistem UNIX nilai deskriptor untuk stdout adalah , dan untuk stderr. Dengan menggunakan fungsi, kami memberi tahu OS untuk mengganti deskriptor lama dengan desciptor baru - yang kami suplai. Misalnya, baris berarti OS menutup deskriptor lama dan menetapkannya ke deskriptor yang merupakan pipa kita. Dengan cara ini anak berpikir bahwa ia menulis ke stdout, bahkan pada kenyataannya ia menulis ke deskriptor pipa kami.execve()
1
2
dup2()
dup2(info->out, 1)
1
info->out
Menjalankan Program Lain dan Membaca Outputnya di Windows
Di dalam implementasi kita, kita harus menetapkan nilai untuk beberapa bidang objek. OS menghubungkan deskriptor pipa kami ke deskriptor standar proses anak. Jika kita menetapkan nilai untuk setidaknya satu deskriptor, kita juga harus mengatur flag, jika tidak OS hanya akan mengabaikan bidang-bidang itu. Kita juga harus memanggil dengan flag untuk setiap deskriptor - proses turunan tidak akan dapat menggunakan deskriptor kita sebaliknya.ps_exec_info()
STARTUPINFOW
STARTF_USESTDHANDLES
SetHandleInformation()
HANDLE_FLAG_INHERIT
ps ps_exec_info(const char *filename, ps_execinfo *ei)
{
...
STARTUPINFOW si = {};
si.cb = sizeof(STARTUPINFO);
if (ei->in != INVALID_HANDLE_VALUE || ei->out != INVALID_HANDLE_VALUE || ei->err != INVALID_HANDLE_VALUE) {
si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
si.dwFlags |= STARTF_USESTDHANDLES;
if (ei->in != INVALID_HANDLE_VALUE) {
si.hStdInput = ei->in;
SetHandleInformation(ei->in, HANDLE_FLAG_INHERIT, 1);
}
if (ei->out != INVALID_HANDLE_VALUE) {
si.hStdOutput = ei->out;
SetHandleInformation(ei->out, HANDLE_FLAG_INHERIT, 1);
}
if (ei->err != INVALID_HANDLE_VALUE) {
si.hStdError = ei->err;
SetHandleInformation(ei->err, HANDLE_FLAG_INHERIT, 1);
}
}
PROCESS_INFORMATION info;
if (!CreateProcessW(wfn, wargs, NULL, NULL, /*inherit handles*/ 1
, 0, /*environment*/ NULL, NULL, &si, &info))
return _PS_NULL;
The parameter "inherit handles" is set to for the child process to correctly inherit our descriptors.1
Result
We've learned how to execute new programs and read what they print to stdout and stderr. As long as the output isn't too large, our program will work just fine.
Reference: ffos/process.h
Getting Current Date/Time
One more important thing - getting the current system date and time. Here's the implementation of which returns the UTC timestamp.time_now()
#ifdef _WIN32
#define TIME_100NS 116444736000000000ULL // 100-ns intervals within 1600..1970
datetime datetime_from_filetime(FILETIME ft)
{
datetime t = {};
unsigned long long i = ((unsigned long long)ft.dwHighDateTime << 32) | ft.dwLowDateTime;
if (i > TIME_100NS) {
i -= TIME_100NS;
t.sec = TIME_1970_SECONDS + i / (1000000 * 10);
t.nsec = (i % (1000000 * 10)) * 100;
}
return t;
}
datetime time_now()
{
FILETIME ft;
GetSystemTimePreciseAsFileTime(&ft);
return datetime_from_filetime(ft);
}
#else
datetime datetime_from_timespec(struct timespec ts)
{
datetime t = {
.sec = TIME_1970_SECONDS + ts.tv_sec,
.nsec = (unsigned int)ts.tv_nsec,
};
return t;
}
/** Get UTC time */
datetime time_now()
{
struct timespec ts = {};
clock_gettime(CLOCK_REALTIME, &ts);
return fftime_from_timespec(&ts);
}
#endif
We're already familiar with functions (see section File Properties), so let's focus on how works. On UNIX we call with flag. On Windows we call . Both functions will return the UTC time configured on our PC. UTC time is best used for program logic or for storing the timestamp in database. Because this timestamp is universal, it doesn't change no matter where (geographically) our program is running right now. However, when we're printing the time to the user, most likely it would be more convenient for him if we print the local time.datetime_from_*()
time_now()
clock_gettime()
CLOCK_REALTIME
GetSystemTimePreciseAsFileTime()
Getting local time zone information
Here's how we can get local time zone offset (UTC+XX):
#include <time.h>
typedef struct {
int real_offset; // offset (seconds) considering DST
} time_zone;
#ifdef _WIN32
void time_local(time_zone *tz)
{
time_t gt = time(NULL);
struct tm tm;
gmtime_s(&tm, >);
time_t lt = mktime(&tm);
tz->real_offset = gt - lt;
}
#else // UNIX:
/** Get local timezone */
void time_local(time_zone *tz)
{
time_t gt = time(NULL);
struct tm tm;
gmtime_r(>, &tm);
time_t lt = mktime(&tm);
tz->real_offset = gt - lt;
}
#endif
Ide dari kode ini adalah bahwa kita mendapatkan stempel waktu UTC dan stempel waktu lokal dan kemudian menghitung perbedaannya:
Dapatkan waktu UTC (
time()
)Dapatkan objek waktu lokal (
gmtime_*()
)Konversikan ke stempel waktu lokal (
mktime()
)Kurangi stempel waktu dan kita sekarang memiliki offset zona waktu lokal
Sekarang, untuk mengonversi UTC ke waktu lokal, kita mengambil waktu UTC dan menambahkan offset zona waktu ke dalamnya. Misalnya, jika dalam UTC dan zona waktu kami adalah CET (, 1 jam lebih awal ketika DST tidak aktif), waktu setempat adalah .2:00
UTC+01
3:00
Hasil
Kami telah belajar cara mendapatkan waktu sistem dan cara mendapatkan offset waktu lokal saat ini.
Referensi: ffos/time.h
Menangguhkan Pelaksanaan Program
Terkadang berguna untuk menangguhkan eksekusi program kita selama beberapa waktu sebelum kontrol sampai ke pernyataan berikutnya dalam kode kita. Misalnya, Anda mungkin ingin mencetak informasi status kepada pengguna program Anda setiap detik. Inilah cara Anda dapat mencapainya:
for (;;) {
const char *status = do_something();
puts(status);
thread_sleep(1000);
}
Kami dapat menerapkan sebagai berikut:thread_sleep()
#ifdef _WIN32
void thread_sleep(unsigned int msec)
{
Sleep(msec);
}
#else
void thread_sleep(unsigned int msec)
{
usleep(msec * 1000);
}
#endif
Kedua fungsi sistem bekerja serupa: mereka memblokir eksekusi program kami sampai jumlah waktu yang ditentukan (dalam milidetik) berlalu. Perhatikan bahwa fungsi-fungsi ini tidak tepat. Mereka mungkin sedikit terlambat, karena satu-satunya hal yang mereka jamin adalah bahwa setidaknya N milidetik harus berlalu sebelum membangunkan proses kami. Tetapi sedikit lebih banyak waktu mungkin benar-benar berlalu sebelum program kita akan menerima kontrol. Ingatlah hal itu.
Hasil
Kami telah belajar cara menangguhkan program kami selama beberapa waktu. Namun, aplikasi yang lebih kompleks biasanya tidak menggunakan pendekatan ini, melainkan menggunakan objek antrian kernel yang membangunkan program setiap kali peristiwa penting perlu diproses. Menggunakan adalah keputusan desain yang buruk, tetapi kami tidak dapat melakukan sesuatu yang lebih baik saat ini, jadi untuk saat ini tidak apa-apa jika Anda menggunakannya untuk program kecil Anda. Namun, jika Anda berpikir bahwa Anda sudah menjadi guru C dan Anda ingin menguji keterampilan Anda dalam pemrograman sistem tingkat lanjut, lanjutkan dan coba tutorial saya yang lain Kernel Queue: Panduan Lengkap Tentang Teknologi Paling Penting Untuk I / O Kinerja Tinggi.thread_sleep()
Kesimpulan
Itu saja untuk tutorial Level 1. Saya sangat berharap Anda telah belajar sesuatu yang menarik dan baru. Masih banyak hal yang belum kita bahas, dan saya akan melakukan yang terbaik untuk menulis tutorial berikutnya, Level 2, di mana kita akan membahas topik pemrograman sistem lintas platform yang lebih sulit.
Posting Komentar untuk "Panduan Pemrograman Sistem Lintas Platform untuk UNIX dan Windows (Level 1)"