進程間通信 (Interprocess communication, IPC)

 在Linux中,進程間通信的方法有多種,像管道,FIFO,共享內存,信號燈還有消息隊列。

管道:
    在此介紹一下有名管道和無名管道兩種,有名管道通常稱爲FIFO,他存在於文件系統中,無名管道沒有名字因爲他們從來沒有路徑名,也不還會在文件系統中出現,嚴格的說,無名管道是和內存中的一個索引節點相關聯的兩個文件描述符,最後的進程關閉了這些文件描述符中的一個,導致索引節點,也就是管道消失,有名管道和無名管道都是半雙工的,也就是說,數據只能由一邊流向令一邊,但不能反過來,另外他們也不能夠用像lseek類似的函數來進行文件的定位。說明一下,下面如果沒有特殊說明,管道說的是無名管道,FIFO說的是有名管道。
    一,打開和關閉管道
        #include <unistd.h>
        int pipe(int filedes[2]);
        在準備向管道中寫數據,或者從管道中讀數據的時候,這個管道必須是存在的,如果pipe函數打開管道成功,則會打開兩個文件描述符,並把他們保存在filedes數組中,其中,filedes[0]用於讀數據,filedes[1]用來寫數據。函數執行成功則會返回0,否則返回-1,並設置errno變量。值得強調的是。這裏的文件描述符不能和一個磁盤文件相對應,他只是主留在內存的一個索引節點。
        要關閉一個管道,只要調用close函數,正常關閉文件描述符。
        #include <stdio.h>
        #include <stdlib.h>
        #include <unistd.h>

        int main(void)
        {
            int fdp[2];

            if (0 > pipe(fdp)) /* open a pipe. */
            {
                perror("creat pipe error\n");
                exit(EXIT_FAILURE);
            }
   
            printf("We get a pipe :\nRead : %d\nWrite : %d\n",
                fdp[0], fdp[1]);
   
            close(fdp[0]); /* close the pipe. */
            close(fdp[1]);
            return 0;
        }

    二,讀寫管道
        由於在使用pipe函數打開的是兩個文件描述符,所以,對管道的讀寫操作,僅需要用對文件描述符讀寫的read函數從filedes[0]中讀數據和write函數從filedes[1]中寫數據,一般的規則是,讀數據的進程關閉管道的寫入端,寫數據的進程關閉管道的讀出端。注意,在關閉管道的寫入端後,從管道讀數據會返回0以示文件末尾,如果關閉讀取端,任何寫入管道的嘗試都會給寫入方產生SIGPIPE信號,而write返回-1,並設置errno爲EPIPE,如果寫入方不能捕獲或者忽略SIGPIPE信號,則寫入進程會中斷。
        如果多個進程向同一個管道寫入數據,則每個進程寫入的數據必須少於PIPE_BUF個字節,PIPE_BUF被定在limits.h中,以保證每個進程每次向管道中寫數據都是一個原子操作,而不會發生數據混亂的現象。
        #include <stdio.h>
        #include <stdlib.h>
        #include <unistd.h>
        #include <sys/types.h>
        #include <sys/wait.h>
        #include <fcntl.h>

        int main(void)
        {
            int prcw[2]; /* parent read, child write. */
            int pwcr[2]; /* parent write, child read. */
            pid_t pid;

            if (0 > pipe(pwcr) || 0 > pipe(prcw))
            {
                perror("Open pipe error\n");
                exit(EXIT_FAILURE);
            }
   
            if (0 > (pid = fork()))
            {
                perror("creat child error\n");
                exit(EXIT_FAILURE);
            }
   
            if (0 == pid)
            {
                char buf[100];
   
                close(prcw[0]); /* send message to parent. */
                write(prcw[1], "Child sent to parent.", sizeof(buf));
                close(prcw[1]);

                close(pwcr[1]); /* read message from parent. */
                read(pwcr[0], buf, 100);
                printf("Child read : %s\n", buf);
                close(pwcr[0]);
            }
            else
            {
                char buf[100];

                close(pwcr[0]); /* sent message to child. */
                write(pwcr[1], "Parent sent to child.", sizeof(buf));
                close(pwcr[1]);
   
                close(prcw[1]); /* read message from child. */
                read(prcw[0], buf, 100);
                printf("Parent read : %s\n", buf);
                close(prcw[0]);

            }
            waitpid(pid, NULL, 0);
            return 0;
        }

    三,標準庫中的管道相關函數
        #include <stdio.h>
        FILE *popen(const char *command, const char *type);
        int pclose(FILE *stream);
        popen函數首先創建一個管道,然後fork出一個子進程,然後調用exec,調用/bin/sh -c執行保存在command中的命令,mode的值必須爲r或者w。如果爲r,那麼popen返回的文件指針用來讀數據,並且,從這個文件中讀出的數據,和command執行的輸出是一樣的。同樣,如果爲w,那麼,對popen函數返回的指針的些操作就是command執行的輸入。如果popen函數失敗,函數返回NULL,並設置errno變量。
        使用pclose來關閉I/O流,他會等待command完成,並向調用進程返回他的退出狀態,如果調用失敗,pclose返回-1。
        #include <stdio.h>
        #include <stdlib.h>

        int main(void)
        {
            char * com = "ps";
            char buf[1024] = {0};
            FILE * pi;
   
            if (NULL == (pi = popen(com, "r"))) /*creat it. */
            {
                perror("popen failed\n");
                exit(EXIT_FAILURE);
            }
   
            fread(buf, 1024, 1, pi); /* read from pipe */
            printf("%s\n", buf);
            pclose(pi);
   
            return 0;
        }

FIFO,有名管道
    一,創建FIFO
        #include <sys/types.h>
        #include <sys/stat.h>
        int mkfifo(const char *pathname, mode_t mode);
        函數mkfifo用來建立一個名爲pathname的FIFO,mode用來指出該FIFO文件文件的權限,通常,mode的值會被程序中的umask修改。如果執行成功,函數返回0,失敗返回-1,並設置errno變量。
    二,操作FIFO
        對FIFO文件的打開,關閉,刪除,讀寫,與普通文件一樣,使用open,close,unlink,read,write來完成。有幾點需要注意,FIFO的兩端在使用之前都必須打開,如果以O_NONBLOCK和O_RDONLY打開,那麼調用會立即返回。如果以O_NONBLOCK和O_WRONLY打開時,調用open會返回一個錯誤,並把errno設置爲ENXIO。另外如果沒有指定O_NONBLOCK,那麼以O_RDONLY打開時,open會被阻塞,直到另一個進程爲寫入數據打開FIFO爲止。O_WRONLY打開時也會被阻塞到爲讀數據打開FIFO爲止。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
    int readfifo;
    int len;
    char buf[1024] = {0};
       
    if (0 > mkfifo("tmp", 0666)) /* creat fifo. */
    {
        perror("Make FIFO error\n");
        exit(EXIT_FAILURE);
    }

    if (0 > (readfifo = open("tmp", O_RDONLY))) /* open FIFO. */
    {
        perror("Read FIFO error\n");
        exit(EXIT_FAILURE);
    }
    puts("We are in ReadFIFO\n");
     /* read FIFO. */
    while (0 < (len = read(readfifo, buf, 1024)))
    {
        printf("Test ");
        printf("%s\n",buf);
    }
    printf("%d",len);
    close(readfifo); /* close FIFO. */
    return 0;
}  

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(void)
{
    int len;
    int wr;
    char buf[1024] = {0};

    if (0 > (wr = open("tmp", O_WRONLY))) /* open FIFO. */
    {
        perror("Open FIFO error\n");
        exit(EXIT_FAILURE);
    }
   
    /* write to FIFO. */
    while (1)
    {
        len = sprintf(buf, "%5d : ---\n", getpid());
        if (0> write(wr, buf, len + 1))
        {
            perror("Write error\n");
            close(wr);
            exit(EXIT_FAILURE);
        }
        puts(buf);
        sleep(3);
    }
   
    close(wr);
    return 0;
}

System V IPC概述
    IPC存在於系統內核,而不是文件系統中,他讓其他無關進程通過一種IPC機制相互進行通信,數據在使用IPC機制的進程間自由流動。每個對象通過他的標識符來引用和訪問,標識符是一個正整數,他唯一的標識出對象本身和他的類型,每個標識符的類型都是唯一的,但是同一標識符的值可以用於一個消息隊列,一個信號燈和一個共享內存區。標識符成爲該結構上所有其他操作的句柄。
    每個IPC結構都由get函數創建,semget用來創建信號燈,msgget用來創建消息隊列,shmget用來創建共享內存,每次調用的時候必須提供一個關鍵字,在創建一個IPC之後,使用同一關鍵字的get函數不會再創建新結構,但會返回和現有結構相關的標識符。這樣就可以使兩個或兩個以上的進程之間建立IPC通道。
    共享內存
        共享內存是內核出於在多個進程間交換信息的目的而預留出的一塊內存區(段),如果段的權限設置的恰當,每個進程都能夠將其映射到自己的私有空間中。如果其中一個進程改變了這會共享內存中的內容,那麼,所有其他的進程將都會看到。
    一,創建共享內存區
        #include <sys/ipc.h>
        #include <sys/shm.h>
        int shmget(key_t key, size_t size, int shmflg);
    其中,shmflg可以是一個或多個IPC_CREAT,IPC_EXCL和一組權限位按位或的結果。權限位以八進制表示,IPC_EXCL在段已經存在的情況下,調用將會失敗而不會返回一個已經存在的標識符。size用來指定大小,但他以PAGE_SIZE爲界.
#include <sys/shm.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int shmid;
   
    if (0 > (shmid = shmget(IPC_PRIVATE, 2048, 0666)))
    {
        perror("Share memery faied\n");
        exit(EXIT_FAILURE);
    }
   
    printf("share memery id : %d\n", shmid);
    system("ipcs -m");
   
    return 0;
}

    二,附加共享內存
        #include <sys/types.h>
        #include <sys/shm.h>
        void *shmat(int shmid, const void *shmaddr, int shmflg);
        int shmdt(const void *shmaddr);
    建立完一塊共享內存之後,進程是不能馬上使用的,必須先將其附加到該進程,同樣,當共享內存使用完畢之後,還要將起與進程分離。其中在附加共享內存時,shmaddr通常爲0,這樣內核會把段映像到調用的進程的地址空間中他所選定的位置。shmflg可以爲SHM_RDONLY,表示只讀,否則,被附加的段默認是可讀可寫的。如果調用成功shmat將返回段地址,否則返回-1,並設置errno變量。shmdt用來將段從進程中分離出去,其中參數shmaddr必須是shmat所返回的地址。
    #include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main(int argc, char * argv[])
{
    int id = atoi(argv[1]);
    char * shm;
   
    if ((char *)0 > (shm = shmat(id, 0, 0)))
    {
        perror("Failed on shmat\n");
        exit(EXIT_FAILURE);
    }
   
    printf("Shared %p\n", shm);
    system("ipcs -m");
    getchar();
    if (0 > shmdt(shm))
    {
        perror("Failed on shmed\n");
        exit(EXIT_FAILURE);
    }
    system("ipcs -m");
    return 0;
   
}
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>

void error(const char * msg);
int main(int argc, char * argv[])
{
    int shmid = atoi(argv[1]);
    char * shm;
    int i = 0;
   
    if ((char *)0 > (shm = shmat(shmid, 0, 0)))
        error("failed on shmat\n");
    printf("Share memery : %p\n", shm);
    if (NULL == (shm = malloc(1024))) /* I don't understand why it
                        malloc. and we do not get
                        the shared memery from shm.*/
        error("failed on malloc");
    printf("Get memery : %p\n", shm);
    while (i < 26)
    {
        shm[i] = 'a' + i;
        i++;
    }
    shm[i] = '\0';
    printf("%s\n",shm);
    free(shm);
//  shmdt(shm); /* is it right after free it? */
            /* and is it safe? */
    return 0;
   
}

void error(const char * msg)
{
    perror(msg);
    exit(EXIT_FAILURE);
}

    消息隊列
        消息隊列是一個信息的鏈接列表,消息都保存在內核中。如果你向隊列中添加一條消息,那麼新的消息將會被加到隊列的尾部,但與FIFO不同的是,你可以隨意的檢索隊列中的消息。
        一,創建和打開消息隊列
            #include <sys/types.h>
            #include <sys/ipc.h>
            #include <sys/msg.h>
            int msgget(key_t key, int msgflg);
        如果msgflg的值爲IPC_PRIVATE,則會創建一個新的消息隊列,如果不是,並且key的值與當前隊列中的值都不相同,並且在msgflg中設置了IPC_CREAT位,那麼也將會創建一個新的隊列,否則將會返回和key有關的已有隊列的ID。如果執行失敗,函數返回-1,並設置errno。
        值得注意的是,在創建System V IPC對象的時候,umask不會修改訪問權限,也就是或,如果不在創建的時候指明權限的話,就會採用默認的權限0,那麼,到時候即使創建者也會無權讀寫該對象。
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/msg.h>

int main(void)
{
    int id;
    key_t key = 1111;

    if (0 > (id = msgget(key, IPC_CREAT | 0666)))
    {
        perror("failed on msgget\n");
        exit(EXIT_FAILURE);
    }
    printf("Queue id : %d\n", id);
    if (0 > (id = msgget(key, 0)))
    {
        perror("failed on second \n");
        exit(EXIT_FAILURE);
    }
    printf("Queue id : %d\n", id);
    return 0;
}


        二,寫入消息
            #include <sys/types.h>
            #include <sys/ipc.h>
            #include <sys/msg.h>
            int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
        函數msgsnd會將一條新的消息添加到隊列的末尾。如果成功將會返回0,否則返回-1,並設置errno。其中,msqid必須是msget返回的隊列ID,msgflg不是0,就是IPC_NOWAIT如果爲0,那麼當系統內消息總數達到最大時,將會被阻塞,直到一條消息被刪除或讀取,而IPC_NOWAIT則是在系統最大數時不等待,直接返回錯誤值。msgsz是你的消息的長度。msgp是指向msgbuf的指針,其定義如下:
            struct msgbuf {
                long mtype;     /* message type, must be > 0 */
                char mtext[1];  /* message data */
                };
        這個結構聲明實際上只是一個模板,你可以對其進行擴展來使其滿足你的需求。例如
        struct msgbuf {
            long mtype;
            int i;
            char c[10];
            };
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct msg {
    long type;
    char msgbuf[1024];
    };

int main(int argc, char * argv[])
{
    int qid = atoi(argv[1]);
    int len = 0;
    struct msg msgb;

    puts("Input message");
    if (NULL == fgets(msgb.msgbuf, 1024, stdin))
    {
        strcpy(msgb.msgbuf, "test...");
    }
    msgb.type = 123;
    len = strlen(msgb.msgbuf);
    if (0 > msgsnd(qid, &msgb, len, 0))
    {
        perror("Failed on msgsnd\n");
        exit(EXIT_FAILURE);
    }
    puts("message posted");
   
    return 0;
}

        三,讀取消息
            #include <sys/types.h>
            #include <sys/ipc.h>
            #include <sys/msg.h>
            ssize_t  msgrcv(int  msqid,  void *msgp, size_t msgsz, long msgtyp, int msgflg);
        msgid用來指明隊列ID,msgp指向得到消息後要填充的結構體,msgsz指明大小,msgty與前面說明的結構體中對應的mtype,如果他是0,則返回隊列中的第一條消息,大於0則返回等於msgtyp的消息,小於0則返回msgtyp小於等於mtype的第一條消息。msgflg個控制msgrcv的行爲,如果msgflg的值爲MSG_NOERROR那麼,如果消息返回的字節比msgsz多,消息就會被截短,否則返回-1,並設置errno爲E2BIG,消息仍然會保存在隊列中。如果是IPC_NOWAIT,如果沒有指定類型的消息那麼就直接返回,並設置errno爲ENOMSG,否則megrsv將會被阻塞知道出現匹配的消息。如果函數執行成功將會從消息隊列中刪除返回的消息。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>

struct msg {
    long type;
    char msgbuf[1024];
    };

int main(int argc, char * argv[])
{
    int id = atoi(argv[1]);
    struct msg msgb;
    int len;
   
    len = msgrcv(id, &msgb, 1024, 0, 0);
    msgb.msgbuf[len] = '\0';
    puts("Get message :");
    puts(msgb.msgbuf);
    return 0;
}

        四,控制消息隊列
            #include <sys/types.h>
            #include <sys/ipc.h>
            #include <sys/msg.h>
            int msgctl(int msqid, int cmd, struct msqid_ds *buf);
        其中cmd的取值可以是:
            IPC_RMID : 刪除隊列msqid
            IPC_STAT : 用隊列的msqid_ds結構填充buf,並讓你查看隊列的內容,但不會刪除任何消息,這是一種非破懷性的讀。
                    struct msqid_ds {
                        struct ipc_perm msg_perm;     /* Ownership and permissions*/
                        time_t         msg_stime;    /* Time of last msgsnd() */
                        time_t         msg_rtime;    /* Time of last msgrcv() */
                        time_t         msg_ctime;    /* Time of last change */
                        unsigned long  __msg_cbytes; /* Current number of bytes in
                                        queue (non-standard) */
                        msgqnum_t      msg_qnum;     /* Current number of messages
                                        in queue */
                        msglen_t       msg_qbytes;   /* Maximum number of bytes
                                        allowed in queue */
                        pid_t          msg_lspid;    /* PID of last msgsnd() */
                        pid_t          msg_lrpid;    /* PID of last msgrcv() */
                        };
            IPC_SET  : 讓你改變隊列的UID,GID,訪問模式和隊列的最大字節數。
    #include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int main(int argc, char * argv[])
{
    int qid = atoi(argv[1]);

    if (0 > msgctl(qid, IPC_RMID, NULL))
    {
        perror("failed on msgctl\n");
        exit(EXIT_FAILURE);
    }
   
    printf("Delete %d successed\n", qid);
   
    return 0;
}

    信號燈
    在這裏只討論最簡單的信號燈,即雙態信號燈,一個雙態信號燈的值只能取兩個值中的一個,即當資源被加鎖不能被其他進程訪問時爲0,而當資源解鎖時爲1。
    一,創建信號燈
        #include <sys/types.h>
        #include <sys/ipc.h>
        #include <sys/sem.h>
        int semget(key_t key, int nsems, int semflg);
    在使用一個信號燈之前必須先創建一個信號燈,semget返回一個和nsems信號燈集合相關的信號燈標識符。如果key爲IPC_PRIVATE或者key還沒有使用,在semflg中設置了IPC_CREAT位,則會創建新的信號燈集合。如果出錯函數返回-1,並設置errno,成功返回和key值相關聯的信號燈標識符。
    二,使用信號燈
        #include <sys/types.h>
        #include <sys/ipc.h>
        #include <sys/sem.h>
        int semop(int semid, struct sembuf *sops, unsigned nsops);
    semid是先前用semget返回的信號燈的標識符,他指向要操作的信號燈的集合。nsops是sops指向的sembuf結構數組的元素的個數,
        struct sembuf {
            unsigned short sem_num;  /* semaphore number */
            short          sem_op;   /* semaphore operation */
            short          sem_flg;  /* operation flags */
            }
    其中sem_op是執行的操作:
        > 0 :  信號燈控制的資源被釋放,而且信號燈的值被增加。
        < 0 :  調用進程將等待直到受控資源被釋放,此時信號燈的值減小,而資源被調用進程加鎖。
        = 0 :  調用進程阻塞直到信號燈變爲0,如果信號燈已經爲0,調用立即返回。
    sem_flg的值可以IPC_NOWAIT或者SEM_UNDO,後者意味着當調用semop的進程退出後執行的操作將被撤銷。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int semid;
    int nsems = 1;
    int flags = 0666;
    struct sembuf buf;
   
    if (0 > (semid = semget(IPC_PRIVATE, nsems, flags)))
    {
        perror("failed on semget\n");
        exit(EXIT_FAILURE);
    }
   
    printf("semid = %d\n", semid);
    system("ipcs -s");
    buf.sem_num = 0;
    buf.sem_op = 1;
    buf.sem_flg = IPC_NOWAIT;
    if (0 > semop(semid, &buf, nsems))
    {
        perror("failed on semop\n");
        exit(EXIT_FAILURE);
    }
    system("ipcs -s");
    return 0;
}

    三,控制信號燈
        #include <sys/types.h>
        #include <sys/ipc.h>
        #include <sys/sem.h>
        int semctl(int semid, int semnum, int cmd, ...);
    cmd的取值如下:
        GETVAL    返回信號燈當前狀態(解鎖或解鎖)
        SETVAL    設置信號燈的狀態爲arg.val
        GETPID    返回上次調用semop進程的PID
        GETNCNT   返回在信號燈上等待的進程數
        GETZCNT   返回等待信號燈值爲0的進程數
        GETALL    返回和semid相關聯的集合中的所有信號燈的進程數
        SETALL    設置和semid相關聯的集合中所有信號燈值爲保存在arg.array中的值
        IPC_RMID  刪除帶有semid的信號燈
        IPC_SET   在信號燈上設置模式
        IPC_STAT  複製配置信息
    如果調用失敗,返回-1,並設置errno
     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) */
           };
    這是semctl函數的第四個參數的模板。
   
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int main(int argc, char * argv[])
{
    int id = atoi(argv[1]);

    if (0 > semctl(id, 0, IPC_RMID))
    {
        perror("failed on semctl\n");
        exit(EXIT_FAILURE);
    }
    printf("Delete %d success!\n", id);
    system("ipcs -s");
   
    return 0;
}
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章