GNU/Linux >> Belajar Linux >  >> Linux

Komunikasi antar-proses di Linux:Soket dan sinyal

Ini adalah artikel ketiga dan terakhir dalam seri tentang komunikasi antarproses (IPC) di Linux. Artikel pertama berfokus pada IPC melalui penyimpanan bersama (file dan segmen memori), dan artikel kedua melakukan hal yang sama untuk saluran dasar:pipa (bernama dan tidak bernama) dan antrian pesan. Artikel ini bergerak dari IPC di ujung atas (soket) ke IPC di ujung bawah (sinyal). Contoh kode menyempurnakan detailnya.

Socket

Sama seperti pipa datang dalam dua rasa (bernama dan tidak bernama), begitu juga soket. Soket IPC (alias soket domain Unix) memungkinkan komunikasi berbasis saluran untuk proses pada perangkat fisik yang sama (host ), sedangkan soket jaringan mengaktifkan jenis IPC ini untuk proses yang dapat berjalan pada host yang berbeda, sehingga membawa jaringan ke dalam permainan. Soket jaringan memerlukan dukungan dari protokol dasar seperti TCP (Transmission Control Protocol) atau UDP (User Datagram Protocol) tingkat yang lebih rendah.

Sebaliknya, soket IPC mengandalkan kernel sistem lokal untuk mendukung komunikasi; khususnya, soket IPC berkomunikasi menggunakan file lokal sebagai alamat soket. Terlepas dari perbedaan implementasi ini, soket IPC dan API soket jaringan pada dasarnya sama. Contoh yang akan datang mencakup soket jaringan, tetapi server sampel dan program klien dapat berjalan pada mesin yang sama karena server menggunakan alamat jaringan localhost (127.0.0.1), alamat untuk mesin lokal di mesin lokal.

Soket yang dikonfigurasi sebagai aliran (dibahas di bawah) adalah dua arah, dan kontrol mengikuti pola klien/server:klien memulai percakapan dengan mencoba menyambung ke server, yang mencoba menerima sambungan. Jika semuanya berfungsi, permintaan dari klien dan tanggapan dari server kemudian dapat mengalir melalui saluran sampai saluran ini ditutup di kedua ujungnya, sehingga memutuskan sambungan.

[Unduh panduan lengkap untuk komunikasi antar-proses di Linux]

Sebuah berulang server, yang hanya cocok untuk pengembangan, menangani klien yang terhubung satu per satu hingga selesai:klien pertama ditangani dari awal hingga akhir, lalu klien kedua, dan seterusnya. Kelemahannya adalah penanganan klien tertentu mungkin macet, yang kemudian membuat semua klien yang menunggu di belakang kelaparan. Server tingkat produksi akan bersamaan , biasanya menggunakan beberapa campuran multi-pemrosesan dan multi-threading. Misalnya, server web Nginx di mesin desktop saya memiliki kumpulan empat proses pekerja yang dapat menangani permintaan klien secara bersamaan. Contoh kode berikut menjaga kekacauan seminimal mungkin dengan menggunakan server berulang; fokusnya tetap pada API dasar, bukan pada konkurensi.

Akhirnya, API soket telah berkembang secara signifikan dari waktu ke waktu karena berbagai penyempurnaan POSIX telah muncul. Kode sampel saat ini untuk server dan klien sengaja dibuat sederhana tetapi menggarisbawahi aspek dua arah dari koneksi soket berbasis aliran. Berikut ringkasan alur kontrol, dengan server dimulai di terminal kemudian klien mulai di terminal terpisah:

  • Server menunggu koneksi klien dan, jika koneksi berhasil, membaca byte dari klien.
  • Untuk menggarisbawahi percakapan dua arah, server menggemakan kembali byte yang diterima dari klien ke klien. Byte ini adalah kode karakter ASCII, yang membentuk judul buku.
  • Klien menulis judul buku ke proses server dan kemudian membaca judul yang sama yang digaungkan dari server. Baik server dan klien mencetak judul ke layar. Berikut adalah output server, pada dasarnya sama dengan klien:
    Listening on port 9876 for clients...
    War and Peace
    Pride and Prejudice
    The Sound and the Fury

Contoh 1. Server soket

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include "sock.h"

void report(const char* msg, int terminate) {
  perror(msg);
  if (terminate) exit(-1); /* failure */
}

int main() {
  int fd = socket(AF_INET,     /* network versus AF_LOCAL */
                  SOCK_STREAM, /* reliable, bidirectional, arbitrary payload size */
                  0);          /* system picks underlying protocol (TCP) */
  if (fd < 0) report("socket", 1); /* terminate */

  /* bind the server's local address in memory */
  struct sockaddr_in saddr;
  memset(&saddr, 0, sizeof(saddr));          /* clear the bytes */
  saddr.sin_family = AF_INET;                /* versus AF_LOCAL */
  saddr.sin_addr.s_addr = htonl(INADDR_ANY); /* host-to-network endian */
  saddr.sin_port = htons(PortNumber);        /* for listening */

  if (bind(fd, (struct sockaddr *) &saddr, sizeof(saddr)) < 0)
    report("bind", 1); /* terminate */

  /* listen to the socket */
  if (listen(fd, MaxConnects) < 0) /* listen for clients, up to MaxConnects */
    report("listen", 1); /* terminate */

  fprintf(stderr, "Listening on port %i for clients...\n", PortNumber);
  /* a server traditionally listens indefinitely */
  while (1) {
    struct sockaddr_in caddr; /* client address */
    int len = sizeof(caddr);  /* address length could change */

    int client_fd = accept(fd, (struct sockaddr*) &caddr, &len);  /* accept blocks */
    if (client_fd < 0) {
      report("accept", 0); /* don't terminate, though there's a problem */
      continue;
    }

    /* read from client */
    int i;
    for (i = 0; i < ConversationLen; i++) {
      char buffer[BuffSize + 1];
      memset(buffer, '\0', sizeof(buffer));
      int count = read(client_fd, buffer, sizeof(buffer));
      if (count > 0) {
        puts(buffer);
        write(client_fd, buffer, sizeof(buffer)); /* echo as confirmation */
      }
    }
    close(client_fd); /* break connection */
  }  /* while(1) */
  return 0;
}

Program server di atas melakukan empat langkah klasik untuk menyiapkan dirinya sendiri untuk permintaan klien dan kemudian menerima permintaan individu. Setiap langkah diberi nama setelah fungsi sistem yang dipanggil server:

  1. soket(…) :dapatkan deskriptor file untuk koneksi soket
  2. mengikat(…) :ikat soket ke alamat di host server
  3. dengarkan(…) :mendengarkan permintaan klien
  4. terima(…) :menerima permintaan klien tertentu

Soket panggilan secara penuh adalah:

int sockfd = socket(AF_INET,      /* versus AF_LOCAL */
                    SOCK_STREAM,  /* reliable, bidirectional */
                    0);           /* system picks protocol (TCP) */

Argumen pertama menentukan soket jaringan yang bertentangan dengan soket IPC. Ada beberapa opsi untuk argumen kedua, tetapi SOCK_STREAM dan SOCK_DGRAM (datagram) kemungkinan yang paling banyak digunakan. Sebuah berbasis aliran socket mendukung saluran yang andal di mana pesan yang hilang atau diubah dilaporkan; salurannya dua arah, dan muatan dari satu sisi ke sisi lain bisa berubah-ubah ukurannya. Sebaliknya, soket berbasis datagram tidak dapat diandalkan (usaha terbaik ), searah, dan membutuhkan muatan berukuran tetap. Argumen ketiga untuk soket menentukan protokol. Untuk soket berbasis aliran yang dimainkan di sini, ada satu pilihan, yang diwakili oleh angka nol:TCP. Karena panggilan berhasil ke soket mengembalikan deskriptor file yang sudah dikenal, soket ditulis dan dibaca dengan sintaks yang sama seperti, misalnya, file lokal.

mengikat panggilan adalah yang paling rumit, karena mencerminkan berbagai penyempurnaan di API soket. Yang menarik adalah bahwa panggilan ini mengikat soket ke alamat memori di mesin server. Namun, mendengarkan panggilan langsung:

if (listen(fd, MaxConnects) < 0)

Argumen pertama adalah deskriptor file soket dan argumen kedua menentukan berapa banyak koneksi klien yang dapat diakomodasi sebelum server mengeluarkan koneksi ditolak kesalahan pada koneksi yang dicoba. (MaxConnects diatur ke 8 di file header sock.h .)

menerima panggilan default ke memblokir menunggu :server tidak melakukan apa pun sampai klien mencoba terhubung dan kemudian melanjutkan. menerima fungsi mengembalikan -1 untuk menunjukkan kesalahan. Jika panggilan berhasil, ia mengembalikan deskriptor file lain—untuk baca/tulis socket berbeda dengan menerima socket direferensikan oleh argumen pertama di terima panggilan. Server menggunakan soket baca/tulis untuk membaca permintaan dari klien dan untuk menulis tanggapan kembali. Soket penerima hanya digunakan untuk menerima koneksi klien.

Secara desain, server berjalan tanpa batas. Oleh karena itu, server dapat dihentikan dengan Ctrl+C dari baris perintah.

Contoh 2. Klien soket

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netdb.h>
#include "sock.h"

const char* books[] = {"War and Peace",
                       "Pride and Prejudice",
                       "The Sound and the Fury"};

void report(const char* msg, int terminate) {
  perror(msg);
  if (terminate) exit(-1); /* failure */
}

int main() {
  /* fd for the socket */
  int sockfd = socket(AF_INET,      /* versus AF_LOCAL */
                      SOCK_STREAM,  /* reliable, bidirectional */
                      0);           /* system picks protocol (TCP) */
  if (sockfd < 0) report("socket", 1); /* terminate */

  /* get the address of the host */
  struct hostent* hptr = gethostbyname(Host); /* localhost: 127.0.0.1 */
  if (!hptr) report("gethostbyname", 1); /* is hptr NULL? */
  if (hptr->h_addrtype != AF_INET)       /* versus AF_LOCAL */
    report("bad address family", 1);

  /* connect to the server: configure server's address 1st */
  struct sockaddr_in saddr;
  memset(&saddr, 0, sizeof(saddr));
  saddr.sin_family = AF_INET;
  saddr.sin_addr.s_addr =
     ((struct in_addr*) hptr->h_addr_list[0])->s_addr;
  saddr.sin_port = htons(PortNumber); /* port number in big-endian */

  if (connect(sockfd, (struct sockaddr*) &saddr, sizeof(saddr)) < 0)
    report("connect", 1);

  /* Write some stuff and read the echoes. */
  puts("Connect to server, about to write some stuff...");
  int i;
  for (i = 0; i < ConversationLen; i++) {
    if (write(sockfd, books[i], strlen(books[i])) > 0) {
      /* get confirmation echoed from server and print */
      char buffer[BuffSize + 1];
      memset(buffer, '\0', sizeof(buffer));
      if (read(sockfd, buffer, sizeof(buffer)) > 0)
        puts(buffer);
    }
  }
  puts("Client done, about to exit...");
  close(sockfd); /* close the connection */
  return 0;
}

Kode setup program klien mirip dengan server. Perbedaan utama antara keduanya adalah bahwa klien tidak mendengarkan atau menerima, melainkan menghubungkan:

if (connect(sockfd, (struct sockaddr*) &saddr, sizeof(saddr)) < 0)

menghubungkan panggilan mungkin gagal karena beberapa alasan; misalnya klien memiliki alamat server yang salah atau terlalu banyak klien yang sudah terhubung ke server. Jika terhubung operasi berhasil, klien menulis permintaan dan kemudian membaca tanggapan yang digaungkan dalam untuk lingkaran. Setelah percakapan, server dan klien menutup soket baca/tulis, meskipun operasi tutup di kedua sisi sudah cukup untuk menutup koneksi. Klien keluar setelah itu tetapi, seperti yang disebutkan sebelumnya, server tetap terbuka untuk bisnis.

Contoh soket, dengan pesan permintaan bergema kembali ke klien, mengisyaratkan kemungkinan percakapan kaya yang sewenang-wenang antara server dan klien. Mungkin ini adalah daya tarik utama soket. Hal ini umum pada sistem modern untuk aplikasi klien (misalnya, klien database) untuk berkomunikasi dengan server melalui soket. Seperti disebutkan sebelumnya, soket IPC lokal dan soket jaringan hanya berbeda dalam beberapa detail implementasi; secara umum, soket IPC memiliki overhead yang lebih rendah dan kinerja yang lebih baik. API komunikasi pada dasarnya sama untuk keduanya.

Sinyal

Sebuah sinyal menginterupsi program yang sedang dieksekusi dan, dalam hal ini, berkomunikasi dengannya. Sebagian besar sinyal dapat diabaikan (diblokir) atau ditangani (melalui kode yang ditentukan), dengan SIGSTOP (jeda) dan SIGKILL (segera diakhiri) sebagai dua pengecualian penting. Konstanta simbolis seperti SIGKILL memiliki nilai integer, dalam hal ini, 9.

Sinyal dapat muncul dalam interaksi pengguna. Misalnya, pengguna menekan Ctrl+C dari baris perintah untuk menghentikan program yang dimulai dari baris perintah; Ctrl+C menghasilkan SIGTERM sinyal. SIGTERM untuk berhenti , tidak seperti SIGKILL , dapat diblokir atau ditangani. Satu proses juga dapat memberi sinyal yang lain, sehingga membuat sinyal menjadi mekanisme IPC.

Pertimbangkan bagaimana aplikasi multi-pemrosesan seperti server web Nginx dapat dimatikan dengan anggun dari proses lain. membunuh fungsi:

int kill(pid_t pid, int signum); /* declaration */

dapat digunakan oleh satu proses untuk menghentikan proses atau kelompok proses lain. Jika argumen pertama berfungsi bunuh lebih besar dari nol, argumen ini diperlakukan sebagai pid (ID proses) dari proses yang ditargetkan; jika argumennya nol, argumen tersebut mengidentifikasi grup proses tempat pengirim sinyal berada.

Argumen kedua untuk membunuh adalah nomor sinyal standar (mis., SIGTERM atau SIGKILL ) atau 0, yang membuat panggilan ke sinyal pertanyaan tentang apakah pid dalam argumen pertama memang valid. Dengan demikian, penutupan aplikasi multi-pemrosesan yang anggun dapat dilakukan dengan mengirimkan berhenti sinyal—panggilan untuk membunuh fungsi dengan SIGTERM sebagai argumen kedua—ke grup proses yang membentuk aplikasi. (Proses master Nginx dapat menghentikan proses pekerja dengan panggilan ke kill dan kemudian keluar dengan sendirinya.) membunuh fungsi, seperti banyak fungsi perpustakaan, menampung kekuatan dan fleksibilitas dalam sintaks pemanggilan sederhana.

Contoh 3. Penghentian sistem multi-pemrosesan yang anggun

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

void graceful(int signum) {
  printf("\tChild confirming received signal: %i\n", signum);
  puts("\tChild about to terminate gracefully...");
  sleep(1);
  puts("\tChild terminating now...");
  _exit(0); /* fast-track notification of parent */
}

void set_handler() {
  struct sigaction current;
  sigemptyset(&current.sa_mask);         /* clear the signal set */
  current.sa_flags = 0;                  /* enables setting sa_handler, not sa_action */
  current.sa_handler = graceful;         /* specify a handler */
  sigaction(SIGTERM, &current, NULL);    /* register the handler */
}

void child_code() {
  set_handler();

  while (1) {   /** loop until interrupted **/
    sleep(1);
    puts("\tChild just woke up, but going back to sleep.");
  }
}

void parent_code(pid_t cpid) {
  puts("Parent sleeping for a time...");
  sleep(5);

  /* Try to terminate child. */
  if (-1 == kill(cpid, SIGTERM)) {
    perror("kill");
    exit(-1);
  }
  wait(NULL); /** wait for child to terminate **/
  puts("My child terminated, about to exit myself...");
}

int main() {
  pid_t pid = fork();
  if (pid < 0) {
    perror("fork");
    return -1; /* error */
  }
  if (0 == pid)
    child_code();
  else
    parent_code(pid);
  return 0;  /* normal */
}

Matikan program di atas mensimulasikan shutdown yang anggun dari sistem multi-pemrosesan, dalam hal ini, yang sederhana terdiri dari proses induk dan proses anak tunggal. Simulasi bekerja sebagai berikut:

  • Proses induk mencoba melakukan fork pada anak. Jika fork berhasil, setiap proses mengeksekusi kodenya sendiri:anak menjalankan fungsi child_code , dan induk menjalankan fungsi parent_code .
  • Proses anak masuk ke loop yang berpotensi tak terbatas di mana anak tidur sebentar, mencetak pesan, kembali tidur, dan seterusnya. Tepatnya SIGTERM sinyal dari orang tua yang menyebabkan anak menjalankan fungsi panggilan balik penanganan sinyal anggun . Dengan demikian, sinyal memecah proses anak keluar dari lingkarannya dan mengatur penghentian yang anggun dari anak dan orang tua. Anak mencetak pesan sebelum mengakhiri.
  • Proses induk, setelah forking anak, tidur selama lima detik sehingga anak dapat mengeksekusi untuk sementara waktu; tentu saja, anak kebanyakan tidur dalam simulasi ini. Orang tua kemudian memanggil membunuh fungsi dengan SIGTERM sebagai argumen kedua, menunggu anak berhenti, lalu keluar.

Berikut adalah output dari contoh run:

% ./shutdown
Parent sleeping for a time...
        Child just woke up, but going back to sleep.
        Child just woke up, but going back to sleep.
        Child just woke up, but going back to sleep.
        Child just woke up, but going back to sleep.
        Child confirming received signal: 15  ## SIGTERM is 15
        Child about to terminate gracefully...
        Child terminating now...
My child terminated, about to exit myself...

Untuk penanganan sinyal, contoh menggunakan sigaction fungsi perpustakaan (disarankan POSIX) daripada sinyal yang lama fungsi, yang memiliki masalah portabilitas. Berikut adalah segmen kode yang menjadi perhatian utama:

  • Jika panggilan ke fork berhasil, induk menjalankan parent_code fungsi dan anak mengeksekusi kode_anak fungsi. Orang tua menunggu selama lima detik sebelum memberi isyarat kepada anak:
    puts("Parent sleeping for a time...");
    sleep(5);
    if (-1 == kill(cpid, SIGTERM)) {
    ...

    Jika membunuh panggilan berhasil, orang tua melakukan menunggu tentang penghentian anak untuk mencegah anak menjadi zombie permanen; setelah menunggu, orang tua keluar.

  • kode_anak fungsi pertama memanggil set_handler dan kemudian masuk ke lingkaran tidur yang berpotensi tak terbatas. Inilah set_handler fungsi untuk ditinjau:
    void set_handler() {
      struct sigaction current;            /* current setup */
      sigemptyset(&current.sa_mask);       /* clear the signal set */
      current.sa_flags = 0;                /* for setting sa_handler, not sa_action */
      current.sa_handler = graceful;       /* specify a handler */
      sigaction(SIGTERM, &current, NULL);  /* register the handler */
    }

    Tiga baris pertama adalah persiapan. Pernyataan keempat menyetel handler ke fungsi anggun , yang mencetak beberapa pesan sebelum memanggil _exit untuk mengakhiri. Pernyataan kelima dan terakhir kemudian mendaftarkan pawang ke sistem melalui panggilan ke sigaction . Argumen pertama untuk sigaction adalah SIGTERM untuk berhenti , yang kedua adalah sigaction current saat ini setup, dan argumen terakhir (NULL dalam hal ini) dapat digunakan untuk menyimpan sigaction sebelumnya setup, mungkin untuk digunakan nanti.

Menggunakan sinyal untuk IPC memang merupakan pendekatan yang minimalis, tetapi merupakan pendekatan yang terbukti benar. IPC melalui sinyal jelas termasuk dalam kotak peralatan IPC.

Menutup seri ini

Ketiga artikel di IPC ini telah membahas mekanisme berikut melalui contoh kode:

  • File yang dibagikan
  • Memori bersama (dengan semafor)
  • Pipa (bernama dan tanpa nama)
  • Antrian pesan
  • Soket
  • Sinyal

Bahkan saat ini, ketika bahasa thread-centric seperti Java, C#, dan Go menjadi sangat populer, IPC tetap menarik karena konkurensi melalui multi-pemrosesan memiliki keunggulan yang jelas dibandingkan multi-threading:setiap proses, secara default, memiliki ruang alamatnya sendiri. , yang mengesampingkan kondisi balapan berbasis memori dalam multi-pemrosesan kecuali jika mekanisme IPC dari memori bersama diterapkan. (Memori bersama harus dikunci dalam multi-pemrosesan dan multi-threading untuk konkurensi yang aman.) Siapa pun yang telah menulis bahkan program multi-threading dasar dengan komunikasi melalui variabel bersama tahu betapa sulitnya menulis thread-safe namun jelas, kode yang efisien. Multi-pemrosesan dengan proses single-threaded tetap menjadi cara yang layak—bahkan cukup menarik—untuk memanfaatkan mesin multi-prosesor saat ini tanpa risiko bawaan kondisi balapan berbasis memori.

Tentu saja, tidak ada jawaban sederhana untuk pertanyaan di antara mekanisme IPC mana yang terbaik. Masing-masing melibatkan trade-off yang khas dalam pemrograman:kesederhanaan versus fungsionalitas. Sinyal, misalnya, adalah mekanisme IPC yang relatif sederhana tetapi tidak mendukung percakapan yang kaya di antara proses. Jika konversi seperti itu diperlukan, maka salah satu pilihan lain lebih tepat. File yang dibagikan dengan penguncian cukup mudah, tetapi file yang dibagikan mungkin tidak berkinerja cukup baik jika proses perlu berbagi aliran data yang besar; pipa atau bahkan soket, dengan API yang lebih rumit, mungkin merupakan pilihan yang lebih baik. Biarkan masalah yang ada memandu pilihan.

Meskipun kode sampel (tersedia di situs web saya) semuanya dalam C, bahasa pemrograman lain sering memberikan pembungkus tipis di sekitar mekanisme IPC ini. Contoh kodenya cukup singkat dan sederhana, saya harap, dapat mendorong Anda untuk bereksperimen.


Linux
  1. Komunikasi antar-proses di Linux:Menggunakan pipa dan antrian pesan

  2. Komunikasi antar-proses di Linux:Penyimpanan bersama

  3. Menyalin pengguna dan kata sandi Linux ke server baru

  1. Memperkenalkan panduan untuk komunikasi antar-proses di Linux

  2. Pantau server linux menggunakan Prometheus dan Grafana

  3. Pantau Server Linux Dengan Prometheus dan Grafana

  1. Nova-agent (Linux) dan agen Rackspace (Windows)

  2. Cara Menginstal CVS dan Membuat Repositori CVS di Server Linux

  3. Cara Install RabbitMQ Server dan Erlang di Linux