
Pada artikel ini, saya ingin berbicara tentang jalur kehidupan proses dalam keluarga OS Linux. Dalam teori dan contoh, saya akan melihat bagaimana proses dilahirkan dan mati, sedikit pembicaraan tentang mekanisme panggilan dan sinyal sistem.
Artikel ini lebih ditujukan untuk pemula dalam pemrograman sistem dan mereka yang hanya ingin belajar lebih banyak tentang bagaimana proses bekerja di Linux.
Semua yang ditulis di bawah ini berlaku untuk Debian Linux dengan kernel 4.15.0.
Isi
- Pendahuluan
- Atribut proses
- Proses siklus hidup
- Proses kelahiran
- Status siap
- Statusnya "sedang berjalan"
- Reinkarnasi di program lain
- Status tertunda
- Hentikan status
- Proses selesai
- Keadaan "zombie"
- Terlupakan
- Ucapan Terima Kasih
Pendahuluan
Perangkat lunak sistem berinteraksi dengan inti sistem melalui fungsi khusus - panggilan sistem. Dalam kasus yang jarang terjadi, ada API alternatif, misalnya, procfs atau sysfs, dibuat dalam bentuk sistem file virtual.
Atribut proses
Proses dalam kernel disajikan hanya sebagai struktur dengan banyak bidang (definisi struktur dapat dibaca di
sini ).
Tetapi karena artikel ini ditujukan untuk pemrograman sistem, dan bukan pengembangan kernel, kami agak abstrak dan hanya fokus pada bidang-bidang penting dari proses bagi kami:
- Id proses (pid)
- Buka Penjelas File (fd)
- Penangan Sinyal
- Direktori kerja saat ini (cwd)
- Variabel Lingkungan (environment)
- Kembali kode
Proses siklus hidup

Proses kelahiran
Hanya satu proses dalam sistem yang dilahirkan dengan cara khusus -
init
- proses ini dihasilkan langsung oleh kernel. Semua proses lain muncul dengan menduplikasi proses saat ini menggunakan
fork(2)
system call. Setelah
fork(2)
dieksekusi, kita mendapatkan dua proses yang hampir identik, dengan pengecualian poin-poin berikut:
fork(2)
mengembalikan PID anak ke orangtua, 0 dikembalikan ke anak;- Anak mengubah PPID (Induk Proses Id) ke PID induk.
Setelah
fork(2)
dieksekusi, semua sumber daya dari proses anak adalah salinan dari sumber daya orang tua. Menyalin suatu proses dengan semua halaman memori yang dialokasikan mahal, sehingga kernel Linux menggunakan teknologi Copy-On-Write.
Semua halaman dalam memori induk ditandai sebagai hanya-baca dan tersedia untuk orang tua dan anak. Segera setelah salah satu proses mengubah data pada halaman tertentu, halaman ini tidak berubah, tetapi salinannya sudah disalin dan diubah. Dalam hal ini, dokumen asli "tidak diikat" dari proses ini. Segera setelah dokumen asli hanya baca tetap "terikat" pada satu proses tunggal, halaman tersebut dialihkan ke status baca-tulis.
Contoh program sederhana yang tidak berguna dengan fork (2) #include <stdio.h> #include <unistd.h> #include <errno.h> #include <sys/wait.h> #include <sys/types.h> int main() { int pid = fork(); switch(pid) { case -1: perror("fork"); return -1; case 0: // Child printf("my pid = %i, returned pid = %i\n", getpid(), pid); break; default: // Parent printf("my pid = %i, returned pid = %i\n", getpid(), pid); break; } return 0; }
$ gcc test.c && ./a.out my pid = 15594, returned pid = 15595 my pid = 15595, returned pid = 0
Status siap
Segera setelah eksekusi,
fork(2)
memasuki status siap.
Bahkan, proses mengantri dan menunggu scheduler di kernel untuk membiarkan proses berjalan pada prosesor.
Statusnya "sedang berjalan"
Segera setelah penjadwal melakukan proses eksekusi, status "running" dimulai. Proses ini dapat menjalankan seluruh periode yang diusulkan (kuantum) waktu, atau dapat memberikan jalan kepada proses lain menggunakan ekspor sistem
sched_yield
.
Reinkarnasi di program lain
Beberapa program menerapkan logika di mana proses induk menciptakan anak untuk memecahkan masalah. Anak dalam hal ini menyelesaikan masalah tertentu, dan orang tua hanya mendelegasikan tugas kepada anak-anaknya. Misalnya, server web dengan koneksi masuk membuat anak dan melewati pemrosesan koneksi ke sana.
Namun, jika Anda perlu menjalankan program lain, Anda harus menggunakan system call
execve(2)
:
int execve(const char *filename, char *const argv[], char *const envp[]);
atau pustaka panggilan
execl(3), execlp(3), execle(3), execv(3), execvp(3), execvpe(3)
:
int execl(const char *path, const char *arg, ... ); int execlp(const char *file, const char *arg, ... ); int execle(const char *path, const char *arg, ... ); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]);
Semua panggilan di atas menjalankan program, jalur yang ditunjukkan dalam argumen pertama. Jika berhasil, kontrol ditransfer ke program yang dimuat dan tidak dikembalikan ke yang asli. Dalam hal ini, program yang dimuat memiliki semua bidang struktur proses, kecuali untuk deskriptor file yang ditandai sebagai
O_CLOEXEC
, mereka akan ditutup.
Bagaimana tidak bingung dalam semua tantangan ini dan memilih yang benar? Cukup memahami logika penamaan:
- Semua panggilan dimulai dengan
exec
- Huruf kelima mendefinisikan jenis argumen yang lewat:
- l singkatan daftar , semua parameter dilewatkan sebagai
arg1, arg2, ..., NULL
- v singkatan vektor , semua parameter dilewatkan dalam array null-dihentikan;
- Huruf p , yang merupakan singkatan dari path , dapat mengikuti. Jika argumen
file
dimulai dengan karakter selain "/", maka file
ditentukan dicari di direktori yang tercantum dalam variabel lingkungan PATH - Yang terakhir mungkin huruf e , menunjukkan lingkungan . Dalam panggilan tersebut, argumen terakhir adalah array null-dihentikan string null-dihentikan dari
key=value
form key=value
- variabel lingkungan yang akan diteruskan ke program baru.
Contoh panggilan ke / bin / cat --help via execve #define _GNU_SOURCE #include <unistd.h> int main() { char* args[] = { "/bin/cat", "--help", NULL }; execve("/bin/cat", args, environ); // Unreachable return 1; }
$ gcc test.c && ./a.out Usage: /bin/cat [OPTION]... [FILE]... Concatenate FILE(s) to standard output. * *
Keluarga panggilan
exec*
memungkinkan Anda untuk menjalankan skrip dengan hak eksekusi dan mulai dengan urutan shebang (#!).
Contoh menjalankan skrip dengan PATH palsu menggunakan execle #define _GNU_SOURCE #include <unistd.h> int main() { char* e[] = {"PATH=/habr:/rulez", NULL}; execle("/tmp/test.sh", "test.sh", NULL, e); // Unreachable return 1; }
$ cat test.sh
Ada konvensi yang menyiratkan bahwa argv [0] cocok dengan argumen nol untuk fungsi dalam keluarga exec *. Namun, ini bisa dilanggar.
Contoh ketika kucing menjadi anjing menggunakan execlp #define _GNU_SOURCE #include <unistd.h> int main() { execlp("cat", "dog", "--help", NULL); // Unreachable return 1; }
$ gcc test.c && ./a.out Usage: dog [OPTION]... [FILE]... * *
Pembaca yang penasaran mungkin memperhatikan bahwa ada nomor di tanda tangan dari fungsi
int main(int argc, char* argv[])
- jumlah argumen, tetapi tidak ada yang semacam itu diteruskan ke keluarga fungsi
exec*
. Mengapa Karena ketika program dimulai, kontrol tidak segera ditransfer ke utama. Sebelum ini, beberapa tindakan yang didefinisikan oleh glibc dilakukan, termasuk menghitung argc.
Status tertunda
Beberapa panggilan sistem dapat berlangsung lama, seperti I / O. Dalam kasus seperti itu, proses masuk ke status "pending". Segera setelah panggilan sistem selesai, kernel akan menempatkan proses dalam status "siap".
Di Linux, ada juga keadaan "menunggu" di mana proses tidak merespons sinyal interupsi. Dalam keadaan ini, proses menjadi "tidak dapat dihancurkan", dan semua sinyal masuk berbaris sampai proses meninggalkan keadaan ini.
Kernel itu sendiri memilih negara mana untuk mentransfer proses. Paling sering, proses yang meminta I / O berada dalam status "menunggu (tanpa gangguan)". Ini terutama terlihat ketika menggunakan disk jarak jauh (NFS) dengan Internet yang tidak terlalu cepat.
Hentikan status
Anda dapat menjeda proses kapan saja dengan mengirimkannya sinyal SIGSTOP. Proses akan masuk ke status "berhenti" dan akan tetap di sana sampai menerima sinyal untuk terus bekerja (SIGCONT) atau mati (SIGKILL). Sinyal yang tersisa akan diantrekan.
Proses selesai
Tidak ada program yang dapat mematikan dirinya sendiri. Mereka hanya dapat meminta sistem untuk ini dengan panggilan sistem
_exit
atau dihentikan oleh sistem karena kesalahan. Bahkan ketika Anda mengembalikan nomor dari
main()
,
_exit
masih dipanggil secara implisit.
Meskipun argumen untuk panggilan sistem adalah int, hanya byte rendah dari nomor tersebut yang diambil sebagai kode pengembalian.
Keadaan "zombie"
Segera setelah proses selesai (tidak peduli apakah itu benar atau tidak), kernel menulis informasi tentang bagaimana proses berakhir dan menempatkannya dalam keadaan "zombie". Dengan kata lain, zombie adalah proses yang selesai, tetapi memori itu masih tersimpan di kernel.
Selain itu, ini adalah kondisi kedua di mana proses dapat dengan aman mengabaikan sinyal SIGKILL, karena tidak dapat mati mati lagi.
Terlupakan
Kode pengembalian dan alasan untuk penyelesaian proses masih disimpan dalam kernel dan harus diambil dari sana. Untuk melakukan ini, Anda dapat menggunakan panggilan sistem yang sesuai:
pid_t wait(int *wstatus); pid_t waitpid(pid_t pid, int *wstatus, int options);
Semua informasi tentang penghentian proses cocok dengan tipe data int. Makro yang dijelaskan dalam halaman manual
waitpid(2)
digunakan untuk mendapatkan kode pengembalian dan alasan penghentian program.
Contoh penyelesaian yang benar dan penerimaan kode pengembalian #include <stdio.h> #include <unistd.h> #include <errno.h> #include <sys/wait.h> #include <sys/types.h> int main() { int pid = fork(); switch(pid) { case -1: perror("fork"); return -1; case 0: // Child return 13; default: { // Parent int status; waitpid(pid, &status, 0); printf("exit normally? %s\n", (WIFEXITED(status) ? "true" : "false")); printf("child exitcode = %i\n", WEXITSTATUS(status)); break; } } return 0; }
$ gcc test.c && ./a.out exit normally? true child exitcode = 13
Contoh penyelesaian salahMelewati argv [0] sebagai NULL menyebabkan crash.
#include <stdio.h> #include <unistd.h> #include <errno.h> #include <sys/wait.h> #include <sys/types.h> int main() { int pid = fork(); switch(pid) { case -1: perror("fork"); return -1; case 0: // Child execl("/bin/cat", NULL); return 13; default: { // Parent int status; waitpid(pid, &status, 0); if(WIFEXITED(status)) { printf("Exit normally with code %i\n", WEXITSTATUS(status)); } if(WIFSIGNALED(status)) { printf("killed with signal %i\n", WTERMSIG(status)); } break; } } return 0; }
$ gcc test.c && ./a.out killed with signal 6
Ada kalanya orang tua berakhir lebih awal dari anak. Dalam kasus seperti itu,
init
akan menjadi orangtua anak dan ia akan menggunakan panggilan
wait(2)
ketika saatnya tiba.
Setelah orangtua mengambil informasi tentang kematian anak itu, kernel menghapus semua informasi tentang anak tersebut sehingga proses lain akan segera terjadi.
Ucapan Terima Kasih
Terima kasih kepada Sasha "Al" untuk pengeditan dan bantuan dalam desain;
Terima kasih kepada Sasha "Reisse" untuk jawaban yang jelas untuk pertanyaan-pertanyaan sulit.
Mereka menerima ilham yang menyerang saya dan rentetan pertanyaan saya yang menyerang saya.