程序間通訊函式介面及應用模板小結

Rice_rice發表於2024-06-04

程序間通訊方式

1.無名管道/有名管道

2.訊號

3.共享佇列(system V-IPC)

4.共享記憶體(system V-IPC)

5.訊號量(system V-IPC)

6.套接字


無名管道特徵

1.檔案沒有名字,無法使用open

2.只能用於親緣程序間

3.半雙工工作方式:讀寫端分開

4.寫入操作不具有原子性,會被打斷,因此只能用於一對一的簡單場景

5.不能使用lseek()來定位

相關API
int pipe(int pipefd[2]);
應用模板
int main(int argc, char *argv[])
{
    int fd[2];
    if (pipe(fd) == -1)
    {
        fprintf(stderr, "errno:%d,%s", errno, strerror(errno));
        exit(-1);
    }
    pid_t x = fork(); // 建立子程序,繼承pipe的描述符
    if (x == 0)       // 子程序
    {
        char *s = "I am the child\n";
        write(fd[1], s, strlen(s)); // 1是讀,0是寫
    }

    if (x > 0) // 父程序
    {
        char buf[30];
        bzero(buf, sizeof(buf));
        read(fd[0], buf, sizeof(buf));
        printf("from the child:%s", buf);
    }
    close(fd[0]); // 因為無名,所以無法用open,但還是需要close
    close(fd[1]);
    return 0;
}


有名管道特徵

1.有名字,且儲存於普通檔案系統中

2.任何有許可權的程序都可以使用open函式獲取FIFO檔案描述符

4.寫入操作具有原子性,支援多寫者同時進行寫操作且資料不會相互踐踏。這是與無名管道的最大區別

5.不能使用lseek()來定位

6.FIFO,最先被寫入的資料,最先被讀出來

相關API
int mkfifo(const char *pathname, mode_t mode);
應用模板
#define FIFO "/tmp/fifo4test"

int main(int argc, char *argv[]) // 傳送的程序
{

    if (access(FIFI, F_OK)) // 檢查檔案是否存在,不存在就建立
    {
        mkfifo(FIFO, 0644);
    }
    int fifo_fd = open(FIFO, O_RDWR);
    char *s = "I am the child\n";
    int n = write(fifo_fd, s, strlen(s)); // 1是讀,0是寫
    printf("%d bytes have been sended.\n", n);
    close(fifo_fd);
    return 0;
}

int main(int argc, char *argv[]) // 接收的程序
{
    if (access(FIFI, F_OK)) // 檢查檔案是否存在,不存在就建立
    {
        mkfifo(FIFO, 0644);
    }
    int fifo_fd = open(FIFO, O_RDWR);

    char buf[30];
    bzero(buf, sizeof(buf));

    read(fifo_fd, buf, sizeof(buf));
    printf("from the child:%s", buf);

    close(fifo_fd);
    return 0;
}

訊號特徵

1.大部分訊號都是非同步

2.linux訊號62個:

1~31是非實時,不可靠訊號,響應不排隊;

如果目標程序沒有及時相應,則隨後到達的同樣的訊號會被丟棄;

每個非實時訊號都對應一個系統事件。

當程序的掛起訊號中含有實時和非實時訊號,則會優先響應實時訊號並從大到小依次響應

34~64實時,可靠訊號,按接收順序排隊

即使相同的實時訊號被同時傳送多次也不會被丟棄,而是依次響應

實時訊號沒有系統事件與之對應

3.對訊號的處理:阻塞,被捕捉並響應(按設定的響應函式或忽略),執行預設動作

相關API
int kill(pid_t pid, int sig);                          // 向pid程序傳送訊號
void (*sighandler_t)(int);                             // 設定的函式,sighandler_t 被定義為指向一個接收單個 int 引數(即訊號編號)並返回 void 的函式的指標。這樣的函式通常被稱為“訊號處理函式”或“訊號處理程式”。
sighandler_t signal(int signum, sighandler_t handler); // 接收signum訊號,並執行handler訊號處理函式,一般和kill配套使用
int raise(int sig);                                    // 給自己傳送訊號
int pause(void);                                       // 掛起等待訊號

sigset_t setset;                                  // 建立訊號集
int sigemptyset(sigset_t *set);                   // 清空訊號集
int sigfillset(sigset_t *set);                    // 將所有訊號新增到訊號集中
int sigaddset(sigset_t *set, int signum);         // 將特定訊號新增到訊號集中
int sigdelset(sigset_t *set, int signum);         // 將特定訊號從訊號集中刪除
int sigismember(const sigset_t *set, int signum); // 判斷某特定訊號是否在訊號集中

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); // 設定阻塞掩碼
應用模板
void handler(int sig)
// sig是觸發該處理函式的訊號值;
// 子程序會不會繼承父程序的訊號響應函式和阻塞狀態,每個程序都有自己的訊號遮蔽字(signal mask),用於控制哪些訊號在當前是阻塞的(即不會被立即處理)。
// 而且不同的訊號能共享一個處理函式
{
    printf("This is a test4signal.sig:%d\n", sig);
}
int main(int argc, char const *argv[])
{
    sigset_t set;                       // 1.建立一個訊號集
    sigemptyset(&set);                  // 清空訊號集
    sigaddset(&set, SIGINT);            // 2.把需要遮蔽的訊號加入到該集合中
    sigprocmask(SIG_BLOCK, &set, NULL); // 3.需要設定該集合的阻塞屬性,第一個引數如果是SIG_UNBLOCK,就解除阻塞
    signal(SIGUSR1, handler);
    // 接收訊號,第二個引數設定成SIG_IGN是忽略  SIG_DEL是刪除
    pause(); // 暫停程序,等待訊號。此處可以用while(1),一直等待訊號

    return 0;
}

訊息佇列,共享記憶體,訊號量 (統稱為system-V IPC)

1.訊息佇列,提供帶有資料標識的特殊管道

2.共享記憶體,提供一塊實體記憶體多次對映到不同的程序虛擬空間

3.訊號量,對程序或執行緒的資源進行管理

訊息佇列相關API
key_t ftok(const char *pathname, int proj_id);                                // 指定路徑和未使用過的整數
int msgget(key_t key, int msgflg);                                            // 獲取訊息佇列的ID
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);            // 傳送訊息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); // 接收訊息
int msgctl(int msqid, int cmd, struct msqid_ds *buf);                         // 設定訊息佇列的屬性

應用模板
// 應用模板:
#define PROJ_PATH "."
#define PROJ_ID 10
#define A2B 1L    // 定義訊息標識
#define MSGSZ 100 // 定義訊息長度

;
struct msgbuf // 定義帶標識的訊息結構體
{
    long mtype;
    char mtext[MSGSZ]; // 訊息最大一般是16384bytes
};

int main()
{
    key_t key = ftok(PROJ_PATH, PROJ_ID);
    int msgid = msgget(key, IPC_CREAT | 0644);
    struct msgbuf buf;
    bzero(&buf, sizeof(buf));
    buf.mtype = A2B;
    strcpy(buf.mtext, "This is a msg test.\n");
    msgsnd(msgid, &buf, strlen(buf.mtext), MSGSZ);
    // 錯誤處理此處忽略,實際專案需要考慮
    //  ================下面為從訊息佇列讀取訊息============//
    struct msgbuf rec;
    bzero(&rec, sizeof(rec));
    msgrcv(msgid, &buf, MSGSZ, A2B, 0); // 0表示等待訊息,預設阻塞
    printf("recive the message:%s", rec.mtext);
    //  ================從訊息佇列讀取訊息============//
    return 0;
}
共享記憶體相關API
key_t ftok(const char *pathname, int proj_id);           // 指定路徑和未使用過的整數
int shmget(key_t key, size_t size, int shmflg);          // 獲取的ID
void *shmat(int shmid, const void *shmaddr, int shmflg); // 對共享記憶體進行對映
int shmdt(const void *shmaddr);                          // 解除對映
int shmctl(int msqid, int cmd, struct msqid_ds *buf);    // 設定共享記憶體的屬性
訊號量相關API
key_t ftok(const char *pathname, int proj_id);           // 指定路徑和未使用過的整數
int semget(key_t key, int nsems, int semflg);            // 獲取訊號量ID
int semop(int semid, struct sembuf *sops, size_t nsops); // 對訊號量進行P/V操作
int semctl(int semid, int semnum, int cmd, ...);         // 獲取或設定訊號量的相關屬性

因為systemV IPC 的訊號量用的較少,且程序間通訊一般可以用POSIX的訊號量代替,因此一起簡要總結後者。

POSIX的訊號量分為有名和無名:有名訊號量,是一種特殊的檔案,一般會放在系統的特殊檔案系統/dev/shm中,不同程序間需要約定一個相同的名字,就能透過這種有名訊號量來相互協調;無名訊號量,一般用於一個程序內執行緒間的同步互斥,因為執行緒共享一個記憶體空間。

POSIX有名訊號量API
sem_t *sem;
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); // 建立/開啟一個有名訊號量
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 申請資源,即P操作
int sem_post(sem_t *sem);                                          // 釋放資源,即V操作
int sem_close(sem_t *sem);                                         // 關閉
int sem_unlink(const char *name);                                  // 刪除有名訊號量檔案

POSIX無名訊號量API

sem_t *sem;
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 申請資源,即P操作
int sem_post(sem_t *sem);                                          // 釋放資源,即V操作
int sem_destroy(sem_t *sem);
/*
有名訊號量和共享記憶體應用模板
#define PROJ_PATH "."
#define PROJ_ID 10
#define SEMNAME "sem4test" // 定義訊息標識
#define SHMSZ 100          // 定義訊息長度
int main()
{
    key_t key = ftok(PROJ_PATH, PROJ_ID);              // 指定路徑和未使用過的整數
    int shm_id = shmget(key, SHMSZ, IPC_CREAT | 0644); // 獲取共享記憶體的ID
    char *shmaddr = shmat(shm_id, NULL, 0);            // 對共享記憶體進行對映

    // 建立POSIX有名訊號量
    sem_t *s;
    s = sem_open(SEMNAME, O_CREAT, 0644, 0);
    while (1)
    {
        fgets(shmaddr, SHMSZ, stdin);
        sem_post(s); // 向訊號量釋放資源,即每次進行操作結束後資源量+1
    }
    sem_unlink(SEMNAME);
    //=========================下半部分為接收訊號=====================//
    while (1)
    {
        sem_post(s); // 向訊號量申請資源,當資訊被讀完,即資訊量為0時被阻塞
        printf("recive message:%s", shmaddr);
    }
    //=========================接收訊號=====================//
    sem_close(s);
    return 0;
}

尤其注意,System V的訊號量和POSIX的訊號量不是一個概念,它們之間存在明顯的區別。以下是兩者之間的主要區別:

  • 來源與標準:

System V訊號量:來源於Unix作業系統的一個分支,即System V版本。

POSIX訊號量:來源於“可移植作業系統介面(Portable Operating System Interface)”標準,這是一個由電氣與電子工程學會(IEEE)開發,並由ISO(國際標準化組織)和IEC(國際電工委員會)採納的國際標準。

  • 使用場景:

System V訊號量:常用於程序間的同步。

POSIX訊號量:常用於執行緒間的同步,但也可以用於程序間同步,特別是當使用有名訊號量時。

  • 實現與複雜性:

System V訊號量:使用相對複雜,通常涉及多個步驟和結構體(如struct sembuf)。

POSIX訊號量:使用相對簡單,透過單一的sem_open呼叫即可完成訊號量的建立、初始化和許可權設定。

  • 儲存位置:

System V訊號量:基於核心,存放在核心空間中。

POSIX訊號量:基於記憶體,訊號量值通常存放在共享記憶體中,有名訊號量透過檔案系統(如/dev/shm)中的特殊檔案來表示。

  • 訊號量型別:

System V訊號量:通常作為訊號量集合存在,每個集合可以包含多個訊號量。

POSIX訊號量:有兩種型別——有名訊號量和無名訊號量。有名訊號量透過IPC名字進行程序間同步,無名訊號量則通常用於執行緒間同步。

  • 訊號量操作:

System V訊號量:透過semop等系統呼叫來操作訊號量。

POSIX訊號量:透過sem_wait(P操作)、sem_post(V操作)等函式來操作訊號量。

  • 標頭檔案與介面:

System V訊號量:使用<sys/sem.h>標頭檔案,介面函式包括semget、semop等。

POSIX訊號量:使用<semaphore.h>標頭檔案,介面函式包括sem_open、sem_wait、sem_post等。


剩下一項套接字,一般用於網路程式設計,即不同主機間的程序通訊,後續補充。

相關文章