Skip to content

CE408-OSL/Experiment05

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

آزمایش شماره ۵ - ارتباط بین پردازه‌ای

  • گروه شماره ۲۵
    • معین آعلی - ۴۰۱۱۰۵۵۶۱
    • ثمین اکبری - ۴۰۱۱۰۵۵۹۴

ایجاد یک Pipe یک‌سویه

ابتدا با زدن دستور man 2 pipe صفحه راهنمای آن را مشاهده و مطالعه می‌کنیم:

حال می‌خواهیم یک pipe یک‌سویه ایجاد کنیم.

دستور pipe دو file descriptor ایجاد می‌کند، یکی از آن‌ها برای خواندن و دیگری برای نوشتن مورد استفاده قرار خواهد گرفت. اولی برای خواندن و دومی برای نوشتن.

با استفاده از کد زیر در پردازه اول یک پیام می‌نویسیم و در پردازه دوم با استفاده از pipe آن را می‌خوانیم:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd[2];
    int res = pipe(fd);

    if (res == -1) {
        perror("pipe");
        exit(1);
    }

    const char *msg = "Hello";
    char buffer[100];

    write(fd[1], msg, strlen(msg) + 1);

    read(fd[0], buffer, sizeof(buffer));

    printf("msg: %s\n", buffer);

    return 0;
}

نتیجه:

نحوه‌ی کار کد بالا به این صورت است. هر چیزی که بر روی fd[1] نوشته شود، قابل خواندن با fd[0] خواهد بود.

حال اگر fork زده شود در این صورت پردازه فرزند هم fd های مربوط به پردازه پدر را دارد:

یک مشکل که داریم این است که در صورتی که هر دو پردازه بخواهند بر روی Pipe بنویسند و یا از آن بخوانند، به دلیل اینکه تنها یک بافر مشترک داریم، رفتار سیستم قابل پیش بینی نخواهد بود. در این حالت یک پردازه ممکن است داده‌ای که خودش بر روی Pipe قرار داده است را بخواند! بنابراین نیاز است که یک طرف تنها بر روی Pipe‌ بنویسد و یک طرف تنها از آن بخواند. برای مثال فرض کنید پردازه‌ی فرزند قصد خواندن از Pipe‌ و پردازه‌ی والد قصد نوشتن بر روی آن را دارد. به کمک فراخوانی سیستمی close ، پردازه والد fd[0] خود را می‌بندد (زیرا قصد خواندن ندارد) و پردازه‌ی فرزند نیز fd[1] را خواهد بست.

انتقال از پردازه پدر به پردازه فرزند

با استفاده از کد زیر عملیات ذکر‌شده‌ی بالا را شبیه‌سازی می‌کنیم:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd[2];
    pid_t pid;

    if (pipe(fd) == -1) {
        perror("pipe");
        exit(1);
    }

    pid = fork();

    if (pid < 0) {
        perror("fork");
        exit(1);
    }

    if (pid > 0) {
        // mother
        close(fd[0]);
        const char *msg = "Hello World!";
        write(fd[1], msg, strlen(msg) + 1);
        close(fd[1]);
    } else {
        // child
        close(fd[1]); 
        char buffer[100];
        read(fd[0], buffer, sizeof(buffer));
        printf("msg from father: %s\n", buffer);
        close(fd[0]); 
    }

    return 0;
}

نتیجه‌ی اجرا:

منطق کد بالا ساده است و در پاراگراف قبلی توضیح داده شده است، فقط باید توجه کنیم که پردازه فرزند دارای pid==0 است.

پیاده‌سازی ls | wc

حال قصد داریم با استفاده از pipe، fork، dup2 و exec ارتباطی بین دو پردازه برقرار کنیم که پدر برنامه ls را اجرا کند و فرزند برنامه‌ی wc را اجرا کند. با این تفاوت که خروجی پدر به عنوان ورودی فرزند در نظر گرفته می‌شود:

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

int main() {
  int fd[2];
  pid_t pid;

  if (pipe(fd) == -1) {
      perror("pipe");
      exit(1);
  }

  pid = fork();

  if (pid < 0) {
      perror("fork");
      exit(1);
  }

  if (pid == 0) {
      // child
      close(fd[1]);
      dup2(fd[0], 0);
      close(fd[0]); 

      execlp("wc", "wc", NULL);

      perror("execlp wc");
      exit(1);
  } else {
      // father
      close(fd[0]);
      dup2(fd[1], 1);
      close(fd[1]);
      
      execlp("ls", "ls", NULL);

      perror("execlp ls");
      exit(1);
  }

  return 0;
}

نتیجه:

ارتباط دوطرفه بین پردازه‌ها

استفاده از pipe تنها یک کانال یک‌طرفه میان دو پردازه به ما می‌دهد و نمی‌توان به تنهایی با آن کانال دو‌طرفه ایجاد کرد.

اما می‌توان برای ارتباط دو طرفه از دو pipe مختلف استفاده کنیم. به این صورت که هر pipe فقط یک ارتباط یک‌طرفه ایجاد می‌کند.

البته باید توجه کنیم که حتما وقتی کار هر پردازه تمام شد از close استفاده کند. در غیر این صورت به deadlock می‌خوریم.

برای شبیه‌سازی این کانال دوطرفه کد زیر را پیاده‌سازی کردیم:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
    int pipe1[2]; // child to father
    int pipe2[2]; // father to child

    if (pipe(pipe1) == -1 || pipe(pipe2) == -1) {
        perror("pipe");
        exit(1);
    }

    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {
        // child
        close(pipe1[1]);
        close(pipe2[0]);

        char buffer[100];
        read(pipe1[0], buffer, sizeof(buffer));
        printf("from father received: %s\n", buffer);

        const char *child_msg = "hello from child";
        write(pipe2[1], child_msg, strlen(child_msg) + 1);

        close(pipe1[0]);
        close(pipe2[1]);
    } else {
        // father
        close(pipe1[0]);
        close(pipe2[1]);

        const char *parent_msg = "hello from father";
        write(pipe1[1], parent_msg, strlen(parent_msg) + 1);

        char buffer[100];
        read(pipe2[0], buffer, sizeof(buffer)); 
        printf("from child received: %s\n", buffer);

        close(pipe1[1]);
        close(pipe2[0]);
    }

    return 0;
}

نتیجه‌ی اجرا:

سیگنال‌ها

با استفاده از دستور man 7 signal لیست سیگنال‌های لینوکس را مشاهده می‌کنیم:

  • SIGINT (Signal Interrupt)

این سیگنال زمانی به پردازه ارسال می‌شود که کاربر به‌صورت دستی قصد توقف اجرای آن را دارد. این سیگنال معمولاً با فشردن کلید ترکیبی Ctrl+C در ترمینال فرستاده می‌شود. رفتار پیش‌فرض آن خاتمه دادن به پردازه است؛ اما برنامه‌ها می‌توانند با تعریف یک handler، این سیگنال را دریافت کرده و به‌جای خاتمه، عملی دیگر انجام دهند کد این سیگنال ۲ است.

  • SIGHUP (Signal Hangup)

این سیگنال در ابتدا زمانی ارسال می‌شد که اتصال ترمینال با پردازه قطع می‌شد (مثلاً وقتی که کاربر ترمینال را می‌بست). رفتار پیش‌فرض آن خاتمه دادن به پردازه است. اما در سیستم‌های مدرن، معمولاً از این سیگنال برای اطلاع‌رسانی به برنامه‌ها برای بارگذاری مجدد تنظیمات استفاده می‌شود؛ به‌ویژه در سرویس‌هایی مانند وب‌سرورها یا daemon‌ها. برنامه‌ها می‌توانند این سیگنال را مدیریت کرده و رفتار دلخواه خود را هنگام دریافت آن اجرا کنند. کد این سیگنال ۹ است.

  • SIGSTOP (Stop)

این سیگنال برای متوقف‌کردن موقتی اجرای یک پردازه استفاده می‌شود. برخلاف دیگر سیگنال‌ها، این سیگنال به‌صورت غیرقابل کنترل است؛ یعنی برنامه دریافت‌کننده نمی‌تواند آن را مسدود کند، نادیده بگیرد یا برای آن handler بنویسد. پس از دریافت این سیگنال، پردازه بلافاصله متوقف می‌شود و اجرای آن تا دریافت سیگنال SIGCONT ادامه نخواهد یافت. این سیگنال معمولاً برای مدیریت پردازه‌ها توسط سیستم‌عامل یا ابزارهای کنترلی استفاده می‌شود. کد این سیگنال معمولا ۱۹ است.

  • SIGCONT (Continue)

این سیگنال برای ادامه‌ی اجرای پردازه‌ای که قبلاً متوقف شده استفاده می‌شود. این سیگنال به پردازه می‌گوید که دوباره اجرا شود. برخلاف SIGSTOP، این سیگنال قابل کنترل و مدیریت است و برنامه‌ها می‌توانند در صورت تمایل، رفتاری خاص در زمان دریافت آن داشته باشند. معمولاً از این سیگنال در کنار SIGSTOP برای مدیریت اجرای پردازه‌ها استفاده می‌شود. کد این سیگنال ۱۸ است.

  • SIGKILL (Kill)

این سیگنال برای پایان فوری و بی‌قیدوشرط یک پردازه استفاده می‌شود. پس از ارسال این سیگنال، پردازه فوراً توسط سیستم‌عامل خاتمه می‌یابد و هیچ‌گونه فرصتی برای ذخیره‌سازی، آزادسازی منابع یا واکنش به آن داده نمی‌شود. این سیگنال قابل مسدود کردن یا مدیریت توسط پردازه نیست و تنها راه مطمئن برای کشتن قطعی یک پردازه محسوب می‌شود. کد این سیگنال ۹ است.

سیگنال Alarm

سیگنال SIGALRM یک سیگنال زمانی است که از سوی تابع alarm به پردازه ارسال می‌شود تا پس از مدت‌زمان مشخصی (برحسب ثانیه) به آن اطلاع دهد. این سیگنال یک تایمر راه‌اندازی می‌کند که پس از گذشت تعداد ثانیه‌های تعیین‌شده، سیگنال SIGALRM را به همان پردازه ارسال می‌کند. رفتار پیش‌فرض SIGALRM خاتمه دادن به پردازه است.

این سیگنال اگر با ورودی ۰ صدا زده شود، تایمرهای قبلی را کنسل می‌کند و اگر با ورودی ۱ صدا زده شود، یک تایمر جدید تنظیم می‌کند. اگر از قبل تایمری فعال باشد آن را کنسل و این تایمر جدید را جایگزین می‌کند.

#include <stdio.h>
#include <unistd.h>
int main() {
        alarm (5);
        printf ("Looping forever . . . \n");
        while (1);
        printf("This line should never be executed\n");
        return 0;
}

کارکرد این برنامه بدین صورت است که ابتدا یک تایمر ۵ ثانیه‌ای تنظیم می‌کند و سپس وارد یک لوپ بی‌نهایت می‌شود. بعد از گذشت ۵ ثانیه سیگنال تایمر می‌رسد و چون برای این برنامه هیچ handler کاستوم‌شده‌ای نوشته نشده، پس رفتار پیش‌فرض خود را انجام می‌دهد و پردازه پایان می‌یابد. در نتیجه هرگز خط اخر پرینت نمی‌شود.

خروجی:

Sigaction

حال می‌خواهیم کدی بنویسیم که پردازه را تا زمان دریافت سیگنال متوقف کند. برای این کار از pause و sigaction استفاده می‌کنیم:

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

void alarm_handler(int signum) {
    printf("signal got!\n");
}

int main() {
    struct sigaction sa;
    sa.sa_handler = alarm_handler;  
    sigemptyset(&sa.sa_mask);       

    if (sigaction(SIGALRM, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    alarm(5);

    printf("waiting ...\n");
    pause();

    printf("after SIGALRM\n");

    return 0;
}

تابع sigaction رفتار پیش‌فرض سیگنال SIGALRM را با تابع دلخواه ما جایگزین می‌کند. همچنین sigemptyset(&sa.sa_mask) برای این است که هنگام توقف سیگنال‌ها را بلاک نکند.

نتیجه‌ی اجرا:

در ادامه می‌خواهیم برنامه‌ای بنویسیم تا کاربر با دو بار فشردن CTRL + C بتواند برنامه را متوقف کند.

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

void handler(int sig) {
    printf("\nsignal got: %d\n", sig);
}

int main() {
    signal(SIGINT, handler);

    printf("start\n");
    pause(); 

    printf("enter CTRL+C again...\n");
    pause(); 

    printf("finished\n");
    return 0;
}

عملکرد برنامه:

About

OS Lab - Experiment05

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages