進程間通信的總結

進程間通信常見的有5種渠道:編程

管道、信號量、共享內存、消息隊列、套接字

下面來一一簡單說明:segmentfault


管道

  • 管道是最簡單方便的一種進程間通信的方式,它本質上是一個fifo文件。又能夠分爲有名管道和無名管道兩種,實質上兩種管道構成沒有區別,可是有名管道是用戶可見的管道,能夠在程序中指明管道文件對其操做,而無名管道則是由系統建立,對於用戶來講是透明的,因此通常來講無名管道只能用來對於有親緣關係的父子進程之間的通訊,而pipe[0]默認是讀端,而pipe[1]默認是寫端。
  • Linux下建立管道文件的命令是: mkfifo (管道文件名)
  • 管道文件是直接寫入內存的,故而管道文件是沒有大小的,可是管道確實有大小的,通常是64KB大小。
  • 管道是一種半雙工的通信方式,一方寫入,一方讀出,它在64KB範圍內循環寫入,寫完一次後會再次返回開始繼續寫入直至一直沒有數據讀出致使寫滿。
  • 這裏指出雙向管道socketpair()並非管道!!!它的本質是兩個套接字,因此會放在套接字部分描述。

兩個簡單的實例:


#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<fcntl.h>
#include<string.h>
#include<signal.h>

//a.c
void fun(int sig)
{
    printf("sig=%d\n", sig);
}

int main()
{
    signal(SIGPIPE, fun);

    int fdw = open("./fifo", O_WRONLY);
    assert(fdw != -1);

    printf("fdw=%d\n", fdw);

    int newfdw = dup(fdw);
    char buff[128] = {0};

//    dup2(fdw, 1);
//    printf("hello");

    while(1)
    {
        printf("input:\n");
        fgets(buff, 128, stdin);
        putchar('\n');

        if(strncmp(buff, "end", 3) == 0)
        {
            break;
        }

        write(newfdw, buff, strlen(buff));
    }

    close(fdw);
    close(newfdw);
}

//b.c
int main()
{
    int fdr = open("./fifo", O_RDONLY);
    assert(fdr != -1);

    printf("fdr=%d\n", fdr);

    char buff[128] = {0};

    while(1)
    {
        memset(buff, 0, 128);
        int len = read(fdr, buff, 127);

        if(len == 0)
        {
            break;
        }

        printf("len=%d, buff=%s\n", len, buff);
    }

    close(fdr);
}

管道文件的使用和普通文件是同樣的,一方讀取,一方寫入,不過在a.c當中有兩個有意思的函數,dup();和dup2();在這個例子中,dup()用newfdw表明了fdw,而dup2()的做用是用fdw去替換標準輸出,有興趣不妨試試,是兩個有趣的函數,這裏很少贅述。數組

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
int main()
{
    int fd[2];
    pipe(fd);
    
    pid_t pid = fork();
    assert(pid != -1);
    
    if(pid == 0)
    {
        close(fd[0]);
        char buff[128] = {0};
        while(1)
        {
            printf("input:\n");
            fgets(buff, 128, stdin);
        
            if(strncmp(buff, "end", 3) == 0)
            {
                break;
            }
    
            write(fd[1], buff, 127);
            sleep(1);
        }
        close(fd[1]);
    }
    else
    {
        close(fd[1]);
        char buff[128] = {0};
        while(1)
        {
            read(fd[0], buff, 127);
            printf("buff=%s\n", buff);
        }
        close(fd[0]);
    }    
}

無名管道是由系統建立的,用戶並不知道它姓甚名誰,也就難以在其餘進程中使用了,但在父子進程中,由於子進程會繼承父進程的文件信息,故而在子進程中仍然存在着無名管道,但在使用中通常防止出錯,乙方負責讀取時必須關閉寫端,一方負責寫入時必須關閉讀端緩存

另外管道的最大長度經過測試可得最大爲64K,可是這並不許確,這個大小應該能夠經過修改Linux下的內核參數來修改最大長度。網絡

信號量

  • 信號量通常用來去同步進程,它能夠控制程序推動的速度。
  • 信號量是一種特殊的變量,它通常只有兩種操做————加1、減一,而這兩個操做都是原子操做,是不可中斷的,當信號量的值爲0時,減一操做會被阻塞,也是由於如此它才能夠控制同步進程,一樣的,也所以信號量必定是一個大於等於0的值。
  • 通常加一操做也叫v操做,它會讓信號量加一(釋放資源),減一操做對應叫p操做,會讓信號量減一(獲取資源)。p操做可能會致使阻塞。

同時在這裏咱們能夠引入一下原子操做,通常原子操做是系統調用中會實現,且原子操做會消耗很大的系統資源,而信號量機制也能夠用於實現原子操做。
貼上3個概念:
臨界資源:同一時刻只容許一個進程訪問的資源
臨界區:訪問臨界資源的代碼段
原子操做:是一種不可分割不可中斷的操做多線程

信號量機制也是實現原子操做一種方式,這些都是爲了去保證在多進程、多線程環境下的內存可見性問題,可是一樣的,這種機制對於計算機資源形成了很大的負擔,這也就產生了CAS操做,https://segmentfault.com/a/11...
關於CAS操做能夠參考一下這篇文章,這裏很少贅述了。dom

原子操做利用信號量機制實現的思路

思路其實很簡單,咱們只須要分辨出臨界區——————必定要注意就是那個同一時間下只能有一個線程去訪問佔有資源的代碼處,在臨界區的上面進行p操做,在臨界區下面則進行v操做便可。socket

簡單實現一個信號量控制:

//sem.h
#include<stdio.h>
#include<sys/sem.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>

union semun
{
    int val;
};

void sem_init();
void sem_p();
void sem_v();
void sem_destroy();

//sem.c
#include"sem.h"

static semid = -1;
void sem_init()
{
    semid = semget((key_t)1234,1,IPC_CREAT | IPC_EXCL | 0600);

    if(semid == -1)
    {
        //已經存在,或者系統資源不足
        //處理已經存在
        semid = semget((key_t)1234, 1, 0600);
        if(semid == -1)
        {
            //確實系統資源不足
            perror("semget error\n");
        }
    }
    else
    {
        union semun a;
        a.val = 1;
        //a.val = 0;
        if(semctl(semid, 0, SETVAL, a) == -1)
        {
            perror("semctl error\n");
        }
    }
}

void sem_p()
{
    struct sembuf buf;
    buf.sem_num = 0;//下標爲0
    buf.sem_op = -1;//p
    buf.sem_flg = SEM_UNDO;//異常結束會將所作操做復原

    if(semop(semid, &buf, 1) == -1)
    {
        perror("sem_p error\n");
    }
}

void sem_v()
{
    struct sembuf buf;
    buf.sem_num = 0;//下標爲0
    buf.sem_op = 1;//v
    buf.sem_flg = SEM_UNDO;//異常結束會將所作操做復原

    if(semop(semid, &buf, 1) == -1)
    {
        perror("sem_p errori\n");
    }
}

void sem_destroy()
{
    if(semctl(semid, 0, IPC_RMID) == -1)
    {
        perror("destroy error\n");
    }
}

//sema.c
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
#include<sys/sem.h>
#include"sem.h"

int main()
{
    sem_init();
    int i;
    for(i = 0; i < 10; i++)
    {
        sem_p();
        write(1,"A",1);
        int n = rand() % 3;
        sleep(n);
        write(1,"A",1);
        sem_v();
        n = rand() % 3;
        sleep(n);
    }

    return 0;
}

這裏面出現了幾個系統調用:
在#include<sys/sem.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, unsigned nsops);
//內部的成員結構:
union semun
{
    short val;//SETVAL用的值
    struct semid_ds *buf;//IPC_STAT、IPC_SET用的semid_ds結構
    unsigned short *array;//SETALL、GETALL用的數組值
    struct seminfo *buf;//爲控制IPC_INFO提供的緩存
};

struct sembuf
{
    short semnum;//信號量集合中的信號量編號,0表明第一個信號量
    short val;//進行P/V操做所加減的值
    short flag;
    /*
    0設置信號量的默認操做
    IPC_NOWAIT設置信號量操做不等待
    SEM_UNDO會讓內核記錄一個與調用進程相關的UNDO記錄,若是該進程崩潰,則根據這個進程的UNDO記錄自動回覆相應信號量的計數值
    */
}
semctl的cmd選項列表
IPC_STAT
從信號量集上檢索semid_ds結構,並存到semun聯合體參數的成員buf的地址中

IPC_SET
設置一個信號量集合的semid_ds結構中ipc_perm域的值,並從semun的buf中取出值post

IPC_RMID
從內核中刪除信號量集合

GETALL
從信號量集合中得到全部信號量的值,並把其整數值存到semun聯合體成員的一個指針數組中

GETNCNT
返回當前等待資源的進程個數

GETPID
返回最後一個執行系統調用semop()進程的PID

GETVAL
返回信號量集合內單個信號量的值

GETZCNT
返回當前等待100%資源利用的進程個數

SETALL
用聯合體中val成員的值設置信號量集合中所有信號量的值

SETVAL
用聯合體中val成員的值設置信號量集合中單個信號量的值

關於這幾個系統調用的詳情可參考於http://blog.csdn.net/guoping1...,對應可理解上面的例子。

另外庫函數裏提供了一個信號量機制實現,能夠方便咱們簡單調用來實行信號量操做。

在#include<semaphore.h>中:

sem_init(sem_t *sem, int pshared, unsigned int value);
pshared 參數指明信號量是由進程內線程共享,仍是由進程之間共享。若是 pshared 的值爲 0,那麼信號量將被進程內的線程共享,而且應該放置在這個進程的全部線程均可見的地址上(如全局變量,或者堆上動態分配的變量)。若是 pshared 是非零值,那麼信號量將在進程之間共享,而且應該定位共享內存區域。
sem_wait(sem_t *sem);//對應P操做
sem_post(sem_t *sem);//對應V操做
//實例1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<unistd.h>
#include<semaphore.h>
#include<pthread.h>

static sem_t sem;
static pthread_mutex_t mutex;

void *fun(void *arg)
{
    int i;
    for(i = 0; i < 10; i++)
    {
        sem_wait(&sem);
        //pthread_mutex_lock(&mutex);

        printf("B");
        fflush(stdout);
        sleep(1);
        printf("B");
        fflush(stdout);

        sem_post(&sem);
        //pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    
    pthread_exit("fun exit\n");
}

int main()
{
//    sem_init(&sem, 0, 1);
    pthread_mutex_init(&mutex, NULL);

    pthread_t id;
    pthread_create(&id, NULL, fun, NULL);

    int i;
    for(i = 0; i < 10; i++)
    {
    //    sem_wait(&sem);
        pthread_mutex_lock(&mutex);

        printf("A");
        fflush(stdout);
        sleep(1);
        printf("A");
        fflush(stdout);

    //    sem_post(&sem);
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    printf("main over\n");

    char *s = NULL;
    pthread_join(id, (void **)&s);
    printf("%s", s);

    exit(0);
}

//實例2
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
#include<semaphore.h>

sem_t sem;

void *fun(void *arg)
{
    char *buff = (char *)arg;
    sem_wait(&sem);
    char *temp = NULL;
    char *token = NULL;
    token = strtok_r(buff, " ", &temp);
    printf("fun token=%s\n", token);
    sleep(1);
    while((token = strtok_r(NULL, " ", &temp)) != NULL)
    {
        printf("fun token=%s\n", token);
        sleep(1);
    }
    sem_post(&sem);
}

int main()
{
    char main_buf[] = "1 2 3 4 5 6 7 8 9 10";
    char fun_buf[] = "A B C D E F G H I J";

    //sem
    sem_init(&sem, 0, 1);

    pthread_t id;
    pthread_create(&id, NULL, fun, (void *)fun_buf);

    //strtok(char *, " ")
    sem_wait(&sem);
    char *token = NULL;
    char *temp = NULL;
    token = strtok_r(main_buf, " ", &temp);
    printf("main token=%s\n", token);
    sleep(1);
    while((token = strtok_r(NULL, " ", &temp)) != NULL)
    {
        printf("main token=%s\n", token);
        sleep(1);
    }
    sem_post(&sem);

    pthread_join(id,NULL);
    exit(0);
}

上面的例子中還用到了互斥鎖,也就是那個mutex,對應兩個函數pthread_mutex_lock(pthread_mutex_t mutex),pthread_mutex_unlock(pthread_mutex_t mutex),和信號量相似,也是在臨界區上方加鎖下方解鎖,這裏下次再說吧這個。

共享內存

  • 共享內存也是做爲一種進程間通信的方式,思路上講和管道類似,就是在內存上申請一份空間讓進程共享這片空間,相似於給進程AB兩我的一塊兒買了一塊祕密基地吧,道理上於管道大同小異,可是它是進程間通信最簡單的一種通信方式,它容許兩個進程同時享用一片內存,因此它在通信時有最高的效率,就像訪問進程本身的空間同樣快捷方便,不須要陷入內核態或者系統調用。
  • 管道、消息隊列等在實現上須要進數據去拷貝在內核當中,再由內核拷貝到接收方,可是共享內存不同,它的實如今效率上實現的最大化,它只用輸入------》共享內存區,共享內存區-----》接收,兩步完成,沒有內核的拷貝,效率得以提高。
  • 共享內存容許兩個或更多進程訪問同一塊內存,就如同 malloc() 函數向不一樣進程返回了指向同一個物理內存區域的指針。當一個進程改變了這塊地址中的內容的時候,其它進程都會察覺到這個更改。
  • 由於系統內核沒有對訪問共享內存進行同步,因此必須提供本身的同步措施。例如,在數據被寫入以前不容許進程從共享內存中讀取信息、不容許兩個進程同時向同一個共享內存地址寫入數據等。解決這些問題的經常使用方法是經過使用信號量進行同步,而信號量見上。

來看看具體的實現吧:

//a
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/shm.h>

int main()
{
    sem_init();

    int shmid = shmget((key_t)1234, 256, IPC_CREAT | 0600);
    assert(shmid != -1);

    char *s = (char *)shmat(shmid, NULL, 0);//NULL不指定進程連接中的地址具體位置,0即便標識位,爲默認)
    assert((int) s != -1);
    //assert(s != (char *)-1);
      
    while(1)
    {
        sem_p(0);

        printf("input str\n");
        char buff[128] = {0};
        fgets(buff, 128, stdin);

        strcpy(s, buff);
        
        sem_v(1);
        if(strncmp(buff, "end", 3) == 0)
        {
            break;
        }
    }
    shmdt(s);//斷開連接s
    exit(0);
}

//b
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/shm.h>

int main()
{
    sem_init();

    int shmid = shmget((key_t)1234, 256, IPC_CREAT | 0600);
    assert(shmid != -1);

    char *s = (char *)shmat(shmid, NULL, 0);//NULL不指定進程連接中的地址具體位置,0即便標識位,爲默認)
    assert((int) s != -1);
    //assert(s != (char *)-1);
    
    while(1)
    {
        sem_p(1);
        if(strncmp(s, "end", 3) == 0)
        {
            break;
        }
        printf("read:%s\n", s);
        sleep(1);
        sem_v(0);
    }

    shmdt(s);//斷開連接s
    sem_destroy();
    exit(0);
}

這裏關於信號量的不在多提,在上面也說到,由於系統內核沒有對訪問共享內存進行同步,因此必須提供本身的同步措施,因此會用到信號量。而且這裏用的信號量是自行實現的,關於實如今前面的實例中有展開。

在頭文件#include<sys/shm.h>中:
int shmget(ket_t key, size_t size, int shmflg);
void shmat(int shmid, const void shmaddr, int shmflg);
int shmdt(const void *shmaddr);

  • 關於shmflg:
  • 0----取共享內存標識符,若不存在函數會報錯
  • IPC_CREAT----當shmflg&IPC_CREAT爲真時,若是內核中不存在鍵值爲key的共享內存,則新建一個共享內存,若是存在則返回該共享內存標識符。
  • IPC_CREAT|IPC_EXCL----若是內核中不存在鍵值爲key的共享內存則新建,若是存在則報錯

詳情可參見:http://blog.csdn.net/guoping1...

消息隊列

  • 消息隊列也屬於進程間通信的一種方式,它於管道相似,但少了打開和關閉管道方面的複雜性。使用消息隊列並未解決咱們在使用命名管道時遇到的一些問題,如管道滿時的阻塞問題。消息隊列提供了一種在兩個不相關進程間傳遞數據的簡單有效的方法。與命名管道相比:消息隊列的優點在於,它獨立於發送和接收進程而存在,這消除了在同步命名管道的打開和關閉時可能產生的一些困難。消息隊列提供了一種從一個進程向另外一個進程發送一個數據塊的方法。並且,每一個數據塊被認爲含有一個類型,接收進程能夠獨立地接收含有不一樣類型值的數據塊。
  • 優勢:

    A. 咱們能夠經過發送消息來幾乎徹底避免命名管道的同步和阻塞問題。
     B. 咱們能夠用一些方法來提早查看緊急消息。
  • 缺點:

    A. 與管道同樣,每一個數據塊有一個最大長度的限制。
     B. 系統中全部隊列所包含的所有數據塊的總長度也有一個上限。
  • Linux系統中有兩個宏定義:
    MSGMAX, 以字節爲單位,定義了一條消息的最大長度。
    MSGMNB, 以字節爲單位,定義了一個隊列的最大長度。
  • 限制:

    因爲消息緩衝機制中所使用的緩衝區爲共用緩衝區,所以使用消息緩衝機制傳送數據時,兩通訊進程必須知足
     以下條件:
    (1)在發送進程把寫入消息的緩衝區掛入消息隊列時,應禁止其餘進程對消息隊列的訪問,不然,將引發消
         息隊列的混亂。同理,當接收進程正從消息隊列中取消息時,也應禁止其餘進程對該隊列的訪問。
    (2)當緩衝區中無消息存在時,接收進程不能接收任何消息;而發送進程是否能夠發送消息,則只由發送進
         程是否可以申請緩衝區決定。

再來看看具體實現:

//msga
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<memory.h>
#include<sys/msg.h>

typedef struct my_message
{
    long int type;
    char msg[128];
}MyMsg;

int main()
{
    int msgid = msgget((key_t)1234, IPC_CREAT | 0600);
    if(msgid == -1)
    {
        perror("a msgget error\n");
    }

    char buff[128];
    int type;
    MyMsg m;
    while(1)
    {
        puts("input type(int)");
        scanf("%d", &type);
        getchar();
    //    fflush(stdin);
        m.type = type;
        
        memset(buff, 0, 128);
        puts("input message");
        fgets(buff, 127, stdin);
        buff[strlen(buff) - 1] = 0;
        strcpy(m.msg, buff);

        if((msgsnd(msgid, &m, sizeof(MyMsg) - sizeof(long), IPC_NOWAIT)) == -1)
        {
            perror("a msgsnd error\n");
        }
        
        if(strncmp(buff, "end", 3) == 0)
        {
            break;
        }
    }
    exit(0);
}

//msgb
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<memory.h>
#include<sys/msg.h>

typedef struct my_message
{
    long int type;
    char msg[128];
}MyMsg;

int main()
{
    int msgid = msgget((key_t)1234, IPC_CREAT | 0600);
    if(msgid == -1)
    {
        perror("b msgget error\n");
    }

    char buff[128];
    int type;
    MyMsg m;
    int i = 0;
    while(1)
    {
        puts("output type(int)");
        scanf("%d", &type);
        fflush(stdin);
        m.type = type;
        
        memset(buff, 0, 128);
        if(msgrcv(msgid, &m, sizeof(MyMsg) - sizeof(long), type, IPC_NOWAIT) == -1)
        {
            perror("b msgrcv error\n");
            continue;
        }
        strcpy(buff, m.msg);
        printf("buff[%d]=%s\n", i++, buff);

        if(strncmp(buff, "end", 3) == 0)
        {
            break;
        }
    }
    msgctl(msgid, IPC_RMID, NULL);
    exit(0);
}

在#inlcude<sys/msg.h>中:

  • int msgget(key_t key, int msgflg);
  • 關於msgflg:
    0----取消息隊列標識符,若不存在函數會報錯
    IPC_CREAT----當msgflg&IPC_CREAT爲真時,若是內核中不存在鍵值爲key的共享內存,則新建一個消息隊列,若是存在則返回該共享內存標識符。
    IPC_CREAT|IPC_EXCL----若是內核中不存在鍵值爲key的消息隊列則新建,若是存在則報錯
  • int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • 關於cmd:
    IPC_STAT:得到msgid的消息隊列第一個消息到buf中。
    IPC_SET:設置消息隊列的屬性,要設置的屬性須要存在buf中。
  • 關於buf結構:

圖片描述

  • int msgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg);
    這其中的msgsz是不包含消息類型的,也就是說消息結構體大小須要減去一個long類型長度的大小!
  • 關於msgflg:
    0----當消息隊列滿時,msgsnd會阻塞,知道消息能寫入。
    IPC_NOWAIT:當消息隊列爲滿時,msgsnd函數不等待當即返回。
    IPC_NOERROR:若發送消息大於size字節,則直接截斷髮送不通知。
  • ssize_t msgrcv(int msgid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  • 關於msgflg:
    0----阻塞式接收消息,沒有改類型的消息則msgrcv一直阻塞。
    IPC_NOWAIT:當消息隊列爲滿時,msgrcv函數不等待當即返回,錯誤碼:ENOMSG。
    IPC_EXCEPT:與msgtype配合試用返回隊列中第一個類型不爲msgtype的消息。
    IPC_NOERROR:若發送消息大於size字節,則直接截斷接收不通知。
  • 關於struct msgbuf
    struct msgbuf{

    long mtype; //這是一個消息中必須包含的消息類型,要求爲long
      ……………………    //具體的消息

    };

詳情可參見:http://blog.csdn.net/guoping1...

套接字

套接字是網絡編程的基礎,是進程間通信中一種經常使用的通信方式,按照Linux下一切皆文件的思想,那麼套接字就能夠看做是一個文件的描述符。這個部分更多的偏向於網絡編程,延伸至select,poll,epoll乃至libevent、boost::asio等等網絡庫,這個放在後面再討論吧。
在這裏額外提一下雙向管道好了,雙向管道,這是一種全雙工的通信方式,雙方均可讀可寫,那麼這也就是普通管道不能知足需求的緣由,因此誕生了一種以套接字爲基礎的輕量級別的解決方案,socketpair();
在#include<sys/socket.h>中:
int socketpair(int domain, int type, int protocol, int sv[2]);
domain參數:選擇協議族 AF_UNIX,AF_LOCAL;
type參數: 選擇類型:SOCK_STREAM,SOCK_DGRAM
pritocal 必須爲0!

這樣處理後,sv數組中會放有兩個鏈接好的套接字文件,這樣經過兩個套接字就能夠互通有無了。

參考資料:
http://blog.csdn.net/Zong__Zo...
http://blog.csdn.net/guoping1...
http://blog.csdn.net/guoping1...
http://blog.csdn.net/guoping1...
http://blog.csdn.net/ttyue_12...

相關文章
相關標籤/搜索