4-bo‘lim

Jarayon yaratish - fork va exec

fork bilan jarayon nusxalash, exec bilan dasturni almashtirish, wait bilan kutish, zombi va yetim jarayonlar.

🕑 19 daqiqa o‘qish 📄 1 250 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. fork - jarayonni ikkiga bo'lish
  2. Bola nimani meros oladi?
  3. Ko'p bola yaratish
  4. exec - dasturni almashtirish
  5. exec oilasi
  6. fork + exec naqshi
  7. wait va waitpid
  8. Zombi jarayon
  9. Yetim jarayon
  10. Demon yaratish
  11. Jarayon yaratish narxi
  12. Xulosa

UNIX'da yangi jarayon yaratishning g'alati, lekin juda kuchli usuli bor: avval o'zingizni nusxalaysiz, keyin nusxani boshqa dasturga almashtirasiz.

fork - jarayonni ikkiga bo'lish #

C
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    printf("Boshlanish\n");
    fflush(stdout);                 /* fork dan oldin buferni bo'shatamiz */

    pid_t natija = fork();

    if (natija < 0) {
        printf("fork muvaffaqiyatsiz\n");
        return 1;
    }

    if (natija == 0) {
        printf("  Men bola jarayonman\n");
        return 0;
    }

    wait(NULL);                     /* bolani kutamiz */
    printf("Men ota jarayonman\n");

    return 0;
}
Natija
Boshlanish
  Men bola jarayonman
Men ota jarayonman
fork() bitta chaqiruv - ikkita qaytish Ota jarayon fork() chaqirildi Ota fork() = 5678 bolaning PID i qaytdi Bola fork() = 0 nol qaytdi Qaytgan qiymatga qarab kim ekaningizni bilasiz
fork chaqiruvdan keyingi qatordan boshlab ikkala jarayonda davom etadi
fork dan oldin fflush chaqiring

fork xotiraning hamma narsasini nusxalaydi - shu jumladan printf ning bo'shatilmagan buferini ham.

fflush bo'lmasa va chiqish faylga yo'naltirilgan bo'lsa, natija shunday chiqadi:

Natija
Boshlanish
  Men bola jarayonman
Boshlanish
Men ota jarayonman

«Boshlanish» ikki marta chiqdi, chunki u fork paytida hali buferda edi. Bola buferning nusxasini oldi va tugaganda uni ham yozdi.

Terminalda bu muammo ko'rinmaydi (u qator bo'yicha buferlaydi), lekin faylga yozganda darhol paydo bo'ladi. Aynan shu sababdan fork dan oldin doim fflush(stdout) chaqiring.

fork ning qaytgan qiymati
QaytdiKim uchunMa'nosi
> 0OtaYaratilgan bolaning PID i
0Bola«Sen bolasan»
< 0OtaXato - jarayon yaratilmadi

Bola o'z PID ini bilishi uchun getpid() chaqiradi. Ota esa fork qaytargan qiymatdan biladi.

Bola nimani meros oladi? #

C
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int global_son = 100;

int main(void) {
    int mahalliy_son = 200;

    pid_t bola = fork();

    if (bola == 0) {
        global_son += 1;
        mahalliy_son += 1;
        printf("  Bola  : global=%d mahalliy=%d\n", global_son, mahalliy_son);
        return 0;
    }

    wait(NULL);
    printf("Ota   : global=%d mahalliy=%d\n", global_son, mahalliy_son);

    return 0;
}
Natija
  Bola  : global=101 mahalliy=201
Ota   : global=100 mahalliy=200
Xotira nusxalanadi, bo'lishilmaydi

Bola otaning xotirasining nusxasini oladi. Bola o'zgaruvchini o'zgartirsa, otaning nusxasi o'zgarmaydi.

Bu C darsligidagi «qiymat bo'yicha uzatish» qoidasiga o'xshaydi - faqat bu yerda butun jarayon nusxalanadi.

NusxalanadiBo'lishiladi
Xotira (butun manzil fazosi)Ochiq fayl deskriptorlari
O'zgaruvchilarFayl ichidagi joriy o'rin
Joriy papkaTerminal

Ma'lumot almashish kerak bo'lsa - quvur (pipe) yoki umumiy xotira kerak. Buni 15-bo'limda ko'ramiz.

Nusxalash aslida darhol sodir bo'lmaydi

1 GB xotira ishlatayotgan jarayonni fork qilsangiz, yadro 1 GB ni darhol ko'chirmaydi - bu juda sekin bo'lardi.

Buning o'rniga copy-on-write (yozishda nusxalash) ishlatiladi:

  1. Ikkala jarayon ham bir xil sahifalarga ko'rsatadi;
  2. Sahifalar «faqat o'qish» deb belgilanadi;
  3. Biri yozmoqchi bo'lganda - yadro o'sha bitta sahifani nusxalaydi.

Shu tufayli fork juda tez ishlaydi. Buni 10-bo'limda batafsil ko'ramiz.

Ko'p bola yaratish #

C
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    for (int i = 1; i <= 3; i++) {
        pid_t bola = fork();

        if (bola < 0) {
            return 1;
        }

        if (bola == 0) {
            printf("  %d-bola ishladi\n", i);
            return 0;               /* bola siklni davom ettirmasin! */
        }

        wait(NULL);                 /* har bir bolani navbatma-navbat kutamiz */
    }

    printf("Barcha bolalar tugadi\n");
    return 0;
}
Natija
  1-bola ishladi
  2-bola ishladi
  3-bola ishladi
Barcha bolalar tugadi
Bolani sikldan chiqarishni unutmang
C
for (int i = 0; i < 3; i++) {
    fork();          /* bola ham siklni davom ettiradi! */
}

Bu kod 3 ta emas, 8 ta jarayon yaratadi (2³). Har bir fork da jarayonlar soni ikki barobar oshadi.

Chegarani olib tashlasangiz - fork bombasi hosil bo'ladi va tizim qotadi. Shuning uchun bola shohobchasi doim return yoki _exit bilan tugashi kerak.

exec - dasturni almashtirish #

C
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    printf("exec dan oldin\n");
    fflush(stdout);                 /* buferni bo'shatamiz */

    pid_t bola = fork();

    if (bola == 0) {
        char *argumentlar[] = {"echo", "  Men endi echo dasturiman", NULL};
        execvp("echo", argumentlar);

        /* bu yerga faqat exec muvaffaqiyatsiz bo'lganda yetamiz */
        printf("  exec ishlamadi\n");
        _exit(1);
    }

    wait(NULL);
    printf("exec dan keyin (ota)\n");

    return 0;
}
Natija
exec dan oldin
  Men endi echo dasturiman
exec dan keyin (ota)
exec jarayonni almashtiradi, yangisini yaratmaydi exec dan oldin PID 5678 Kod: mening dasturim Xotira: mening ma'lumotim exec exec dan keyin PID 5678 (o'zgarmadi!) Kod: /bin/echo Xotira: butunlay yangi PID, ochiq fayllar va joriy papka saqlanadi - qolgani almashadi
exec muvaffaqiyatli bo'lsa, keyingi qatorga hech qachon qaytmaydi
exec dan keyingi kod bajarilmaydi
C
execvp("echo", argumentlar);
printf("Bu chiqmaydi\n");      /* exec muvaffaqiyatli bo'lsa - yo'q */

exec joriy jarayonning butun kodini almashtiradi. Qaytadigan joy qolmaydi.

Shuning uchun exec dan keyingi kod - bu xato ishlovchi. U faqat exec muvaffaqiyatsiz bo'lganda (dastur topilmadi, huquq yo'q) bajariladi.

exec oilasi #

FunksiyaArgumentlarPATH da qidiradi
execlRo'yxat: execl(yol, a0, a1, NULL)yo'q
execlpRo'yxatha
execvMassiv: execv(yol, argv)yo'q
execvpMassivha
execveMassiv + muhityo'q
C
execl("/bin/ls", "ls", "-l", NULL);              /* to'liq yo'l */
execlp("ls", "ls", "-l", NULL);                  /* PATH dan topadi */

char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv);                              /* massiv bilan */
Nomlardagi harflar
  • l (list) - argumentlar ro'yxat bo'lib beriladi;
  • v (vector) - argumentlar massiv bo'lib beriladi;
  • p (path) - dasturni PATH o'zgaruvchisidan qidiradi;
  • e (environment) - muhit o'zgaruvchilari ham beriladi.

Amalda execvp eng ko'p ishlatiladi.

fork + exec naqshi #

Bu naqsh butun UNIX'ning asosi. Terminalda buyruq yozganingizda aynan shu sodir bo'ladi.

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

int buyruq_bajar(char *argumentlar[]) {
    fflush(stdout);                 /* bola buferni takrorlamasin */

    pid_t bola = fork();

    if (bola < 0) {
        return -1;
    }

    if (bola == 0) {
        execvp(argumentlar[0], argumentlar);
        _exit(127);                 /* exec ishlamadi */
    }

    int holat;
    waitpid(bola, &holat, 0);

    if (WIFEXITED(holat)) {
        return WEXITSTATUS(holat);
    }

    return -1;
}

int main(void) {
    char *birinchi[] = {"echo", "Birinchi buyruq", NULL};
    char *ikkinchi[] = {"echo", "Ikkinchi buyruq", NULL};
    char *xato[] = {"bunday_buyruq_yoq_12345", NULL};

    printf("Natija 1: %d\n", buyruq_bajar(birinchi));
    printf("Natija 2: %d\n", buyruq_bajar(ikkinchi));
    printf("Natija 3: %d\n", buyruq_bajar(xato));

    return 0;
}
Natija
Birinchi buyruq
Natija 1: 0
Ikkinchi buyruq
Natija 2: 0
Natija 3: 127
Nima uchun fork va exec alohida?

Windows'da CreateProcess bitta chaqiruvda hamma narsani qiladi. UNIX ularni ataylab ajratgan.

Sabab: oraliqdagi joyda ko'p narsa qilish mumkin.

fork va exec orasida bola:

  • fayl deskriptorlarini qayta yo'naltirishi (> va | shunday ishlaydi);
  • huquqlarini pasaytirishi;
  • muhit o'zgaruvchilarini o'zgartirishi;
  • joriy papkani almashtirishi mumkin.

Shuning uchun qobiqdagi ls > natija.txt shunday oddiy ishlaydi - bola exec dan oldin chiqishni faylga bog'laydi.

wait va waitpid #

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

int main(void) {
    pid_t bola = fork();

    if (bola == 0) {
        printf("  Bola 42 kodi bilan tugaydi\n");
        exit(42);
    }

    int holat;
    waitpid(bola, &holat, 0);

    if (WIFEXITED(holat)) {
        printf("Bola normal tugadi, kodi: %d\n", WEXITSTATUS(holat));
    } else if (WIFSIGNALED(holat)) {
        printf("Bola signal bilan to'xtadi: %d\n", WTERMSIG(holat));
    }

    return 0;
}
Natija
  Bola 42 kodi bilan tugaydi
Bola normal tugadi, kodi: 42
MakrosNima aytadi
WIFEXITED(h)Normal tugadimi?
WEXITSTATUS(h)Qaytgan kod (0-255)
WIFSIGNALED(h)Signal bilan to'xtadimi?
WTERMSIG(h)Qaysi signal
WIFSTOPPED(h)To'xtatildimi?
holat - oddiy son emas
C
int holat;
wait(&holat);
printf("%d\n", holat);        /* 42 emas, 10752 chiqadi! */

holat ichida bir necha ma'lumot bitlarga joylashtirilgan: chiqish kodi, signal raqami, core dump bo'lgani.

Shuning uchun uni doim makroslar orqali o'qing.

Zombi jarayon #

C
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    pid_t bola = fork();

    if (bola == 0) {
        printf("  Bola darhol tugaydi\n");
        return 0;
    }

    /* Ota bolani kutmasdan ishlashda davom etadi.
       Shu payt bola ZOMBI holatida. */
    sleep(1);

    printf("Ota endi bolani o'qiydi\n");
    wait(NULL);                     /* zombi tozalandi */
    printf("Zombi yo'q qilindi\n");

    return 0;
}
Natija
  Bola darhol tugaydi
Ota endi bolani o'qiydi
Zombi yo'q qilindi
Ikki maxsus holat Zombi (zombie) Bola tugadi, ota hali o'qimadi • Xotirasi bo'shatilgan • PCB yozuvi qolgan • PID band turadi Yechim: ota wait() chaqirsin Yetim (orphan) Ota tugadi, bola davom etmoqda • Bola ishlashda davom etadi • systemd (PID 1) uni asrab oladi • PPID 1 ga o'zgaradi Muammo emas - bu normal Zombi - muammo; yetim - odatiy hol (demonlar shunday yaratiladi)
Bitta zombi zararsiz, minglab zombi esa PID larni tugatadi
Terminal
ps aux | awk '$8 ~ /^Z/ {print}'
Zombi to'planishi

Server dasturi har bir mijoz uchun fork qilib, wait chaqirmasa, zombi lar to'planadi. PID lar tugagach, tizim yangi jarayon yarata olmaydi.

Uch yechim:

  1. wait chaqirish - eng oddiy, lekin ota bloklanadi;
  2. SIGCHLD ishlovchisi - bola tugaganda avtomatik tozalash;
  3. Ikki marta fork - bolani darhol yetim qilib, systemd ga topshirish.
C
signal(SIGCHLD, SIG_IGN);      /* eng oson: bolalarni o'zi tozalasin */

Signallarni 16-bo'limda batafsil ko'ramiz.

Yetim jarayon #

C
#include <stdio.h>
#include <unistd.h>

int main(void) {
    pid_t bola = fork();

    if (bola == 0) {
        pid_t oldingi_ota = getppid();
        sleep(2);                   /* ota bu vaqtda tugaydi */
        pid_t yangi_ota = getppid();

        printf("  Bola: ota o'zgardimi? %s\n",
               oldingi_ota != yangi_ota ? "ha" : "yo'q");
        printf("  Bola: yangi ota PID 1 mi? %s\n",
               yangi_ota == 1 ? "ha" : "yo'q (init boshqa jarayon)");
        return 0;
    }

    printf("Ota tugadi (bola hali ishlayapti)\n");
    return 0;
}
Natija
Ota tugadi (bola hali ishlayapti)
  Bola: ota o'zgardimi? ha
  Bola: yangi ota PID 1 mi? yo'q (init boshqa jarayon)
Yangi ota kim bo'ladi?

An'anaviy UNIX'da yetim jarayonni PID 1 (init yoki systemd) asrab oladi.

Zamonaviy Linux'da esa subreaper mexanizmi bor: sessiya boshqaruvchisi (masalan systemd --user yoki konteyner boshlig'i) o'zini «asrab oluvchi» deb belgilashi mumkin. Shunda yetim unga o'tadi, PID 1 ga emas.

Shuning uchun yuqoridagi misolda javob muhitga qarab o'zgaradi.

Demon yaratish #

C
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>

int demon_bol(void) {
    pid_t bola = fork();

    if (bola < 0) {
        return -1;
    }

    if (bola > 0) {
        _exit(0);                   /* ota chiqadi - bola yetim bo'ladi */
    }

    /* Endi biz yetimmiz. Yangi sessiya boshlaymiz. */
    if (setsid() < 0) {
        return -1;
    }

    /* Ikkinchi fork - terminalga qayta ulanib qolmaslik uchun */
    bola = fork();

    if (bola < 0) {
        return -1;
    }

    if (bola > 0) {
        _exit(0);
    }

    umask(0);
    if (chdir("/") < 0) {
        return -1;
    }

    return 0;
}

int main(void) {
    printf("Demon yaratish qadamlari tayyor\n");
    printf("Haqiqiy demon uchun demon_bol() chaqiriladi\n");
    return 0;
}
Natija
Demon yaratish qadamlari tayyor
Haqiqiy demon uchun demon_bol() chaqiriladi
Demon nima?

Demon (daemon) - orqa fonda ishlaydigan, terminalga bog'lanmagan jarayon. sshd, cron, nginx - hammasi demon.

Klassik demon yaratish qadamlari:

  1. fork va otaning chiqishi - qobiq buyruqni tugagan deb hisoblaydi;
  2. setsid - yangi sessiya, terminaldan uzilish;
  3. Ikkinchi fork - endi hech qachon terminal ololmaydi;
  4. chdir("/") - fayl tizimini band qilib turmaslik;
  5. umask(0) - fayl huquqlarini o'zi belgilash;
  6. stdin, stdout, stderr ni /dev/null ga yo'naltirish.

Zamonaviy Linux'da bularni systemd o'zi qiladi - siz oddiy dastur yozasiz, u esa fon rejimini boshqaradi. 18-bo'limda ko'ramiz.

Jarayon yaratish narxi #

C
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <time.h>

#define MARTA 200

int main(void) {
    struct timespec boshlandi, tugadi;

    clock_gettime(CLOCK_MONOTONIC, &boshlandi);

    for (int i = 0; i < MARTA; i++) {
        pid_t bola = fork();

        if (bola == 0) {
            _exit(0);
        }

        wait(NULL);
    }

    clock_gettime(CLOCK_MONOTONIC, &tugadi);

    double vaqt = (double) (tugadi.tv_sec - boshlandi.tv_sec) +
                  (double) (tugadi.tv_nsec - boshlandi.tv_nsec) / 1e9;

    printf("%d ta jarayon yaratildi\n", MARTA);
    printf("Vaqt musbatmi? %s\n", vaqt > 0.0 ? "ha" : "yo'q");

    /* aniq raqamni ko'rish uchun: */
    /* printf("Bittasiga o'rtacha: %.1f mikrosoniya\n", vaqt / MARTA * 1e6); */

    return 0;
}
Natija
200 ta jarayon yaratildi
Vaqt musbatmi? ha

Izohdagi qatorni yoqsangiz, bitta jarayon yaratish odatda 50-500 mikrosoniya olishini ko'rasiz. Bu oqim (thread) yaratishdan ancha qimmat - shuning uchun keyingi bo'limda oqimlarni ko'ramiz.

Amaliy topshiriq
  1. fork bilan bola jarayon yarating va ikkalasining PID sini chiqaring.
  2. fork dan keyin o'zgaruvchini o'zgartirib, ota ko'radimi tekshiring.
  3. Sikl ichida fork qilib, return siz nechta jarayon paydo bo'lishini hisoblang.
  4. execvp bilan ls -l ni ishga tushiring.
  5. exec dan keyingi printf bajarilmasligini tasdiqlang.
  6. waitpid va WEXITSTATUS bilan bolaning chiqish kodini oling.
  7. Bolani kutmasdan zombi yarating va ps da toping.
  8. SIGCHLD ni SIG_IGN qilib, zombi yo'qolishini kuzating.
  9. Yetim jarayon yaratib, uning PPID i o'zgarishini ko'ring.
  10. fork va oddiy funksiya chaqiruvi tezligini taqqoslang.

Xulosa #

  • fork joriy jarayonning to'liq nusxasini yaratadi.
  • U bir marta chaqiriladi, ikki marta qaytadi: otaga bolaning PID i, bolaga 0.
  • Bola xotiraning nusxasini oladi - o'zgarishlar bo'lishilmaydi.
  • Amalda nusxalash copy-on-write bilan kechiktiriladi.
  • Sikl ichida fork qilsangiz, bolani return bilan chiqaring.
  • exec jarayonning kodini almashtiradi, yangi jarayon yaratmaydi.
  • exec muvaffaqiyatli bo'lsa, keyingi qatorga qaytmaydi.
  • fork + exec - UNIX'ning asosiy naqshi; oradagi joy qayta yo'naltirish uchun ishlatiladi.
  • wait bolani kutadi va uning yozuvini tozalaydi.
  • Kutilmagan bola - zombi; otasiz bola - yetim.
  • Zombi to'planishi PID larni tugatib, tizimni to'xtatadi.

Keyingi bo'limda oqimlarni - bitta jarayon ichidagi parallel ishni ko'ramiz.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.