Linux 進程間通信的六種常見方式及代碼實現

進程間通信(IPC):


進程間通信的方式有很多,這裏主要講到進程間通信的六種方式,分別爲:管道、FIFO、消息隊列、共享內存、信號、信號量。

一、管道


管道的特點:

  1. 是一種半雙工的通信方式;

  2. 只能在具有親緣關係的進程間使用. 進程的親緣關係一般指的是父子關係;

  3. 它可以看成是一種特殊的文件,對於它的讀寫也可以使用普通的 read、write 等函數。但是它不是普通的文件,並不屬於其他任何文件系統,並且只存在於內存中。

管道的原型:

 #include <unistd.h>
int pipe(int pipefd[2]);

代碼實現:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
 
/*使用匿名管道實現進程間通信*/
int main()
{
        int fd[2];//fd[0]爲讀端 fd[1]爲寫端
        pid_t pid;
        char buf[128];
        //int pipe(int pipefd[2]);
        if(pipe(fd) == -1)//創建管道
        {
                printf("管道創建失敗\n");
                perror("why");
        }
 
        pid = fork();
 
        if(pid < 0 )
        {
                printf("子進程開闢失敗\n");
                perror("why");
        }else if(pid > 0){
 
                sleep(3);//讓子進程先執行
                printf("這是一個父進程\n");//父進程完成寫操作
                close(fd[0]);
                write(fd[1],"hello from father",strlen("hello from father"));
        }else{
 
                printf("這是一個子進程\n");//子進程完成讀操作
                close(fd[1]);
                read(fd[0],buf,sizeof(buf));//沒有數據來時,阻塞在這
                printf("buf = %s\n",buf);
        }
 
        return 0;
}

二、FIFO


FIFO,也叫做命名管道,它是一種文件類型。

FIFO 的特點:

  1. FIFO 可以在無關的進程之間交換數據,與無名管道不同;

  2. FIFO 有路徑名與之相關聯,它以一種特殊設備文件形式存在於文件系統中。

FIFO 的原型:

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

其中的 mode 參數與 open 函數中的 mode 相同。一旦創建了一個 FIFO,就可以用一般的文件 I/O 函數操作它。

當 open 一個 FIFO 時,是否設置非阻塞標誌(O_NONBLOCK)的區別:

代碼實現:

下列代碼有效解決了,當管道存在時,程序報錯的問題,減少了無關錯誤信息的打印。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
//       int mkfifo(const char *pathname, mode_t mode);
 
int main()
{
        if(mkfifo("myfifo",0600) == -1 && errno != EEXIST)
        {
                printf("mkfifo failed\n");
                perror("why");
        }
 
        return 0;
}

read.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
 
//       int mkfifo(const char *pathname, mode_t mode);
 
int main()
{
        int nread;
        char buf[30] = {'\0'};
 
        if(mkfifo("myfifo",0600) == -1 && errno != EEXIST)//創建命名管道
        {
                printf("mkfifo failed\n");
                perror("why");
        }
 
        int fd = open("./myfifo",O_RDONLY);//以只讀的形式打開管道,程序阻塞在這,直到有另一個進程對其執行寫操作
        if(fd < 0)
        {
                printf("read open failed\n");
        }else
        {
                printf("read open successn\n");
        }
 
        while(1)
        {
                nread = read(fd,buf,sizeof(buf));
                printf("read %d byte,context is:%s\n",nread,buf);
        }
 
        close(fd);
 
        return 0;
}

write.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
//       int mkfifo(const char *pathname, mode_t mode);
 
int main()
{
        int nread;
        char buf[30] = "message from myfifo";
 
        if(mkfifo("myfifo",0600) == -1 && errno != EEXIST)//創建命名管道
        {
                printf("mkfifo failed\n");
                perror("why");
        }
 
        int fd = open("./myfifo",O_WRONLY);//打開管道,程序阻塞在這,直到其他進程爲讀而打開它
        if(fd < 0)
        {
                printf("write open failed\n");
        }
        else
        {
                printf("write open success\n");
        }
 
        while(1)
        {
                sleep(1);
                write(fd,buf,strlen(buf));
        }
        close(fd);
 
        return 0;
}

三、消息隊列

消息隊列,是消息的鏈接表,存放在內核之中。一個消息隊列由一個標識符(即隊列 ID)來標識。

用戶進程可以向消息隊列添加消息,也可以向消息隊列讀取消息。

消息隊列的特點:

  1. 消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級;

  2. 消息隊列是獨立於發送和接收進程的,進程終止時,消息隊列及其內容並不會被刪除;

  3. 消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。

消息隊列函數的原型:

// 創建或打開消息隊列:成功返回隊列ID,失敗返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失敗返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 讀取消息:成功返回消息數據的長度,失敗返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息隊列:成功返回0,失敗返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

代碼演示:

msgSend.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//       int msgget(key_t key, int msgflg);
// 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);
struct msgbuf{
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
};
 
 
int main()
{
        struct msgbuf sendbuf={888,"message from send"};
        struct msgbuf readbuf;
 
        key_t key;
 
        if((key = ftok(".",'z')) < 0){
                printf("ftok error\n");
        }
        int msgId = msgget(key,IPC_CREAT|0777);
 
        if(msgId == -1){
                printf("get quen failed\n");
        }
 
        msgsnd(msgId,&sendbuf,strlen(sendbuf.mtext),0);
        printf("send over\n");
 
        msgrcv(msgId,&readbuf,sizeof(readbuf.mtext),999,0);
        printf("read from get is:%s\n",readbuf.mtext);
 
        msgctl(msgId,IPC_RMID,NULL);
 
        return 0;
}

msgGet.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//       int msgget(key_t key, int msgflg);
// 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);
struct msgbuf{
        long mtype;       /* message type, must be > 0 */
        char mtext[128];    /* message data */
};
 
int main()
{
        struct msgbuf readbuf;
        memset(readbuf.mtext, '\0', sizeof(readbuf.mtext));
        struct msgbuf sendbuf={999,"thank for your reach"};
 
        key_t key;
 
        //獲取key值
        if((key = ftok(".",'z')) < 0){
                printf("ftok error\n");
        }
 
        int msgId = msgget(key,IPC_CREAT|0777);
 
        if(msgId == -1){
                printf("get quen failed\n");
                perror("why");
        }
 
        msgrcv(msgId,&readbuf,sizeof(readbuf.mtext),888,0);
        printf("read from send is:%s\n",readbuf.mtext);
 
        msgsnd(msgId,&sendbuf,strlen(sendbuf.mtext),0);
 
        msgctl(msgId,IPC_RMID,NULL);
 
        return 0;
}

四、共享內存


共享內存,指兩個或多個進程共享一個給定的存儲區。

ipcs -m 查看系統下已有的共享內存;ipcrm -m shmid 可以用來刪除共享內存。

共享內存的特點:

  1. 共享內存是最快的一種 IPC,因爲進程是直接對內存進行存取。

  2. 因爲多個進程可以同時操作,所以需要進行同步。

  3. 信號量 + 共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問。

共享內存函數的原型:

// 創建或獲取一個共享內存:成功返回共享內存ID,失敗返回-1
int shmget(key_t key, size_t size, int flag);
// 連接共享內存到當前進程的地址空間:成功返回指向共享內存的指針,失敗返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 斷開與共享內存的連接:成功返回0,失敗返回-1
int shmdt(void *addr); 
// 控制共享內存的相關信息:成功返回0,失敗返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

代碼演示:

shmw.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
 
//       int shmget(key_t key, size_t size, int shmflg);
// void *shmat(int shmid, const void *shmaddr, int shmflg);
 
//       int shmdt(const void *shmaddr);
 
int main()
{
        int shmId;
        key_t key;
        char *shmaddr;
 
        if((key = ftok(".",1)) < 0){
                printf("ftok error\n");
        }
 
        shmId = shmget(key, 1024*4, IPC_CREAT|0666);//內存大小必須得是MB的整數倍
 
        if(shmId == -1){
                printf("shmget error\n");
                exit(-1);
        }
 
        /*第二個參數一般寫0,讓linux內核自動分配空間,第三個參數也一般寫0,表示可讀可寫*/
        shmaddr = shmat(shmId, 0, 0);
        printf("shmat OK\n");
        strcpy(shmaddr,"I am so cool");
 
        sleep(5);//等待5秒,讓別的進程去讀
 
        shmdt(shmaddr);
        shmctl(shmId, IPC_RMID, 0);//寫0表示不關心
        printf("quit\n");
 
        return 0;
}

shmr.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
 
//       int shmget(key_t key, size_t size, int shmflg);
// void *shmat(int shmid, const void *shmaddr, int shmflg);
 
//       int shmdt(const void *shmaddr);
 
int main()
{
        int shmId;
        key_t key;
        char *shmaddr;
 
        if((key = ftok(".",1)) < 0){
                printf("ftok error\n");
        }
 
        shmId = shmget(key, 1024*4, 0);//內存大小必須得是MB的整數倍
 
        if(shmId == -1){
                printf("shmget error\n");
                exit(-1);
        }
 
        /*第二個參數一般寫0,讓linux內核自動分配空間,第三個參數也一般寫0,表示可讀可寫*/
        shmaddr = shmat(shmId, 0, 0);
        printf("shmat OK\n");
        printf("data : %s\n",shmaddr);
 
        shmdt(shmaddr);
 
        return 0;
}

五、信號


對於 Linux 來說,實際信號是軟中斷,許多重要的程序都需要處理信號。終端用戶輸入了 ctrl+c 來中斷程序,會通過信號機制停止一個程序。

信號的相關概述:

1、信號的名字和編號:

每個信號都有一個名字和編號,這些名字都以 “SIG” 開頭。我們可以通過 kill -l 來查看信號的名字以及序號。

不存在 0 信號,kill 對於 0 信號有特殊的應用。

2、信號的處理:

信號的處理有三種方法,分別是:忽略、捕捉和默認動作。

信號處理函數的註冊:

  1. 入門版:函數 signal

  2. 高級版:函數 sigaction

信號處理發送函數:

  1. 入門版:kill

  2. 高級版:sigqueue

入門版:

函數原型:

 //接收函數,第二個參數指向信號處理函數

sighandler_t signal(int signum, sighandler_t handler);

//發送函數
 int kill(pid_t pid, int sig);

接收端:

#include <stdio.h>
#include <signal.h>
 
//       typedef void (*sighandler_t)(int);
 
//       sighandler_t signal(int signum, sighandler_t handler);
/*接受到信號後,讓信號處理該函數*/
void handler(int signum)
{
        printf("signum = %d\n",signum);
 
        switch(signum){
                case 2:
                        printf("SIGINT\n");
                        break;
                case 9:
                        printf("SIGKILL\n");
                        break;
                case 10:
                        printf("SIGUSR1\n");
                        break;
        }
}
 
int main()
{
        signal(SIGINT,handler);
        signal(SIGKILL,handler);
        signal(SIGUSR1,handler);
 
        while(1);
 
        return 0;
}

發送端:

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
 
//       int kill(pid_t pid, int sig);
 
int main(int argc,char **argv)
{
        int signum;
        int pid;
 
        signum = atoi(argv[1]);//將字符型轉爲整型
        pid = atoi(argv[2]);
 
        kill(pid,signum);
 
        printf("signum = %d,pid = %d\n",signum,pid);
 
        return 0;
}

高級版:

函數原型:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
 
struct sigaction {
   void       (*sa_handler)(int); //信號處理程序,不接受額外數據,SIG_IGN 爲忽略,SIG_DFL 爲默認動作
   void       (*sa_sigaction)(int, siginfo_t *, void *); //信號處理程序,能夠接受額外數據和sigqueue配合使用
   sigset_t   sa_mask;//阻塞關鍵字的信號集,可以再調用捕捉函數之前,把信號添加到信號阻塞字,信號捕捉函數返回之前恢復爲原先的值。
   int        sa_flags;//影響信號的行爲SA_SIGINFO表示能夠接受數據
 };
//回調函數句柄sa_handler、sa_sigaction只能任選其一

我們只需要配置 sa_sigaction 以及 sa_flags 即可。

siginfo_t {
               int      si_signo;    /* Signal number */
               int      si_errno;    /* An errno value */
               int      si_code;     /* Signal code */
               int      si_trapno;   /* Trap number that caused
                                        hardware-generated signal
                                        (unused on most architectures) */
               pid_t    si_pid;      /* Sending process ID */
               uid_t    si_uid;      /* Real user ID of sending process */
               int      si_status;   /* Exit value or signal */
               clock_t  si_utime;    /* User time consumed */
               clock_t  si_stime;    /* System time consumed */
               sigval_t si_value;    /* Signal value */
               int      si_int;      /* POSIX.1b signal */
               void    *si_ptr;      /* POSIX.1b signal */
               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
               int      si_timerid;  /* Timer ID; POSIX.1b timers */
               void    *si_addr;     /* Memory location which caused fault */
               int      si_band;     /* Band event */
               int      si_fd;       /* File descriptor */
}
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
   int   sival_int;
   void *sival_ptr;
 };

接收端:

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
 
//       int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
 
//(*sa_sigaction)(int, siginfo_t *, void *);
void handler(int signum, siginfo_t *info, void *context)
{
        printf("get signum is:%d\n",signum);
 
        if(context != NULL)
        {
                printf("get data = %d\n",info->si_int);
                printf("get data = %d\n",info->si_value.sival_int);
                printf("get pid is = %d\n",info->si_pid);
        }
 
}
 
int main()
{
        struct sigaction act;
        printf("pid = %d\n",getpid());
        act.sa_sigaction = handler;
        act.sa_flags = SA_SIGINFO;
 
        sigaction(SIGUSR1,&act,NULL);
        while(1);
 
        return 0;
}

發送端:

#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
//       int sigqueue(pid_t pid, int sig, const union sigval value);
 
int main(int argc,char **argv)
{
        int signum;
        int pid;
 
        signum = atoi(argv[1]);
        pid = atoi(argv[2]);
 
        union sigval value;
        value.sival_int = 100;
 
        sigqueue(pid,signum,value);
        printf("pid = %d,done\n",getpid());
 
        return 0;
}

注意:信號發送字符串,只有在父子進程或者是共享內存下才可發送。

六、信號量


信號量與已經介紹過的 IPC 結構不同,它是一個計數器。信號量用於實現進程間的互斥與同步,而不是用於存儲進程間通信數據。

信號量的特點:

  1. 信號量用於進程間同步,若要在進程間傳遞數據需要結合共享內存。

  2. 信號量基於操作系統的 PV 操作,程序對信號量的操作都是原子操作。

  3. 每次對信號量的 PV 操作不僅限於對信號量值加 1 或減 1,而且可以加減任意正整數。

  4. 支持信號量組

信號量的函數原型:

// 創建或獲取一個信號量組:若成功返回信號量集ID,失敗返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 對信號量組進行操作,改變信號量的值:成功返回0,失敗返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);  
// 控制信號量的相關信息
int semctl(int semid, int sem_num, int cmd, ...);

當 semget 創建新的信號量集合時,必須指定集合中信號量的個數(即 num_sems),通常爲 1; 如果是引用一個現有的集合,則將 num_sems 指定爲 0 。

在 semop 函數中,sembuf 結構的定義如下:

struct sembuf 
{
    short sem_num; // 信號量組中對應的序號,0~sem_nums-1
    short sem_op;  // 信號量值在一次操作中的改變量
    short sem_flg; // IPC_NOWAIT, SEM_UNDO
}

在 semctl 函數中的命令有多種,這裏就說兩個常用的:

代碼演示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
 
//       int semget(key_t key, int nsems, int semflg);
//       int semctl(int semid, int semnum, int cmd, ...);
//       int semop(int semid, struct sembuf *sops, size_t nsops);
union semun{
        int              val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
        struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                    (Linux-specific) */
};
 
//P操作,拿鑰匙
void PGetKey(int semid)
{
        struct sembuf sops;
        sops.sem_num = 0;
        sops.sem_op = -1;
        sops.sem_flg = SEM_UNDO;
 
        semop(semid, &sops, 1);
}
 
//V操作,放回鑰匙
void VPutBackKey(int semid)
{
        struct sembuf sops;
        sops.sem_num = 0;
        sops.sem_op = 1;
        sops.sem_flg = SEM_UNDO;
 
        semop(semid, &sops, 1);
}
 
int main()
{
        key_t key;
        int semid;
        if((key == ftok(".",6)) < 0)
        {
                printf("ftok error\n");
        }
 
        semid = semget(key , 1,  IPC_CREAT|0666);//創造鑰匙,數量爲1
 
        union semun sem;
        sem.val = 0;//初始狀態爲沒有鑰匙
        semctl(semid, 0, SETVAL, sem);//SETVAL初始化信號量爲一個已知的值,這時就需要第四個參數
                     //0表示操作第一把鑰匙
        int pid = fork();
 
        if(pid < 0)
        {
                printf("fork failed\n");
        }else if(pid == 0)
        {
                printf("this is child\n");
                VPutBackKey(semid);//首先把鑰匙放回     
        }else
        {
                PGetKey(semid);//等子進程將鑰匙放回後纔會進行操作,保證子進程先執行
                printf("this is father\n");
                VPutBackKey(semid);
                semctl(semid, 0, IPC_RMID);//銷燬鑰匙
        }
 
        return 0;
}

七、進程間通信方式總結:


  1. 管道:速度慢,容量有限,只有父子進程能通訊;

  2. FIFO:任何進程間都能通訊,但速度慢;

  3. 消息隊列:容量受到系統限制,且要注意第一次讀的時候,要考慮上一次沒有讀完數據的問題;

  4. 共享內存:能夠很容易控制容量,速度快,但要保持同步,比如一個進程在寫的時候,另一個進程要注意讀寫的問題;

  5. 信號:有入門版和高級版兩種,區別在於入門版注重動作,高級版可以傳遞消息。只有在父子進程或者是共享內存中,纔可以發送字符串消息;

  6. 信號量:不能傳遞複雜消息,只能用來同步。用於實現進程間的互斥與同步,而不是用於存儲進程間通信數據。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qz5bJ_y2NGWuWdC3U3u0lQ