linux進程通信


linux常用進程通信方式包括管道(pipe)、有名管道(FIFO)、信號(signal)、消息隊列、共享內存、信號量、套接字(socket)。

管道

管道是單向、先進先出的無結構的字節流。用於父子進程之間的通信。關鍵系統調用如下:
int pipe( int fd[2] );fd數組用於返回兩個fd,分別表示通道的兩端。
int main(){
    int pid;
    int fd[2];
    if(pipe(fd)<0){//父進程創建管道
        perror("Fail to pipe");
        exit(EXIT_FAILURE);
    }
    if((pid=fork())<0){
        perror("Fail to fork");
        exit(EXIT_FAILURE);
    }else if(pid == 0){
        close(fd[1]);//表示管道的方向,fd[1]用於寫
        child_read_pipe(fd[0]);//子進程讀取管道
    }else{
        close(fd[0]);//fd[0]用於讀
        father_write_pipe(fd[1]);//父進程寫入管道
    }
}

有名管道

有名管道以設備文件的形式存在,可被任意知道名字的進程使用,而不止在只有親緣關系的進程之間。
要使用有名管道,必須先建立它,並與他的一段相連,才能打開進行讀寫。當文件不再需要時,要顯示刪除。系統調用:
int mknod( char *pathname, mode_t mode, dev_t dev);
server端,創建管道:
#define FIFO_FILE       "MYFIFO"
//有名管道server端
int main(void)
{
        FILE *fp;
        char readbuf[80];

        /* Create the FIFO if it does not exist */
        umask(0);
        mknod(FIFO_FILE, S_IFIFO|0666, 0);

        while(1)
        {
                fp = fopen(FIFO_FILE, "r");
                fgets(readbuf, 80, fp);
                printf("Received string: %s\n", readbuf);
                fclose(fp);
        }

        return(0);
}
client端:
//有名管道client端
int main(int argc, char *argv[])
{
        FILE *fp;

        if ( argc != 2 ) {
                printf("USAGE: fifoclient [string]\n");
                exit(1);
        }

        if((fp = fopen(FIFO_FILE, "w")) == NULL) {
                perror("fopen");
                exit(1);
        }

        fputs(argv[1], fp);

        fclose(fp);
        return(0);
}
有名管道創建后,以設備文件形式存在,標志位有p。
 

信號

一個進程發出信號,另一個進程捕獲此信號並作出動作。比如linux下ctrl+z快捷鍵終止進程,其實是向進程發送了SIGINT信號,進程捕獲該信號並終止。
常用信號如下:
SIGALRM 由alarm函數的定時器產生。
SIGHUP SIGHUP和控制台操作有關,當控制台被關閉時系統會向擁有控制台sessionID的所有進程發送HUP信號,默認HUP信號的action是 exit,如果遠程登陸啟動某個服務進程並在程序運行時關閉連接的話會導致服務進程退出,所以一般服務進程都會用nohup工具啟動或寫成一個 daemon。
SIGINT 鍵盤中斷信號,比ctrl+z
SIGKILL 強制kill進程,不可被捕獲或忽略。
SIGPIPE Broken Pipe,向通道寫時沒有對應的讀進程。
SIGTERM 要求進程結束運行,linux系統關機時會發送此信號,kill命令默認也是發送此信號。
SIGUSER1 SIGUSER2 進程之間用這兩個信號通信。
注冊信號處理函數:
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
signum為信號,act為包含新的信號處理函數,oldact保存舊的信號處理函數。
sigaction結構體定義如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_handler和sa_sigaction為信號處理函數,一般以聯合體的形式存在,由sa_handler指定的處理函數只有一個參數,即信號值,所以信號不能傳遞除信號值之外的任何信息;由sa_sigaction指定的信號處理函數帶有三個參數,是為實時信號而設的(當然同樣支持非實時信號),它指定一個3參數信號處理函數。第一個參數為信號值,第三個參數沒有使用(posix沒有規范使用該參數的標准),第二個參數是指向siginfo_t結構的指針,結構中包含信號攜帶的數據值。
siginfo_t結構如下,siginfo_t結構中的si_value要么持有一個4字節的整數值,要么持有一個指針,這就構成了與信號相關的數據
typedef struct {
int si_signo;
int si_errno;
int si_code;
union sigval si_value;
} siginfo_t;
union sigval {
int sival_int;
void *sival_ptr;
}
sigset_t信號集用來描述信號的集合,linux所支持的所有信號可以全部或部分的出現在信號集中,主要與信號阻塞相關函數配合使用。sa_mask指定在信號處理程序執行過程中,哪些信號應當被阻塞。缺省情況下當前信號本身被阻塞,防止信號的嵌套發送,除非指定SA_NODEFER或者SA_NOMASK標志位。
sa_flags中包含了許多標志位,比較重要的標志位是SA_SIGINFO,表示信號處理函數是用sa_handler還是sa_sigaction。
指定信號處理函數實例如下:
void ouch(int sig)
{
    printf("\nOUCH! - I got signal %d\n", sig);
}
 
int main()
{
    struct sigaction act;
    act.sa_handler = ouch;
    //創建空的信號屏蔽字,即不屏蔽任何信息
    sigemptyset(&act.sa_mask);
    //使sigaction函數重置為默認行為
    act.sa_flags = SA_RESETHAND;
 
    sigaction(SIGINT, &act, 0);
 
    while(1)
    {
        printf("Hello World!\n");
        sleep(1);
    }
    return 0;
} 
 
發送信號系統調用:
int kill(pid_t pid, int sig);
alarm函數在經過預定時間后向發送一個SIGALRM信號,seconds為0表示所有已設置的鬧鍾請求。
unsigned int alarm(unsigned int seconds);
static int alarm_fired = 0;
 
void ouch(int sig)
{
    alarm_fired = 1;
}
 
int main()
{
    //關聯信號處理函數
    signal(SIGALRM, ouch);
    //調用alarm函數,5秒后發送信號SIGALRM
    alarm(5);
    //掛起進程
    pause();
    //接收到信號后,恢復正常執行
    if(alarm_fired == 1)
        printf("Receive a signal %d\n", SIGALRM);
    exit(0);

}
 

消息隊列

消息隊列是存在於內核的鏈表結構。消息含有一個類型,接收進程可以獨立地接收含有不同類型的數據結構。消息隊列的消息不能超過4056 bytes。
消息隊列相關的系統調用如下:
int msgget(key_t key, int msgflg);
創建或獲取隊列,key用來標識隊列,msgflag表示消息隊列的訪問權限,和文件的訪問權限一樣。msgflg可以與IPC_CREAT做或操作,表示當key所命名的消息隊列不存在時創建一個消息隊列,如果key所命名的消息隊列存在時,IPC_CREAT標志會被忽略,而只返回一個標識符。
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
發送消息,msgid表示消息隊列標識,msg_ptr表示消息指針,msg_ptr所指向結構體第一個成員必須為長整型消息類型,如下所示,mtext除可為char數組類型外,還可為任意其它類型
struct msgbuf {
long mtype; /* type of message */
char mtext[1]; /* message text */
};
msg_sz表示消息長度,不包括類型字段,msgflagmsgflg用於控制當前消息隊列滿或隊列消息到達系統范圍的限制時將要發生的事情。
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);
接收消息,msgtype>0表示接收消息的類型,msgtype為0表示不分類型接收第一個消息。如果它小於零,就獲取類型等於或小於msgtype的絕對值的第一個消息。其它參數和msgsend類似。
int msgctl(int msgid, int command, struct msgid_ds *buf);
msgctl用於控制消息隊列,每個消息隊列在內核中存在相應的數據結構msgid_ds,其結構如下:
/* one msqid structure for each queue on the system */
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue */
struct msg *msg_last; /* last message in queue */
time_t msg_stime; /* last msgsnd time */
time_t msg_rtime; /* last msgrcv time */
time_t msg_ctime; /* last change time */
struct wait_queue *wwait;
struct wait_queue *rwait;
ushort msg_cbytes;
ushort msg_qnum;
ushort msg_qbytes; /* max number of bytes on queue */
ushort msg_lspid; /* pid of last msgsnd */
ushort msg_lrpid; /* last receive pid */
};
command是將要采取的動作,它可以取3個值:
IPC_STAT 獲取消息隊列在內核中的msgid_ds結構,並存儲到buf中。
IPC_SET 設置消息隊列的ipc_perm成員
IPC_RMID 從內核中移除消息隊列。
接受者:
struct msg_st
{
    long int msg_type;
    char text[BUFSIZ];
};

int main()
{
    int running = 1;
    int msgid = -1;
    struct msg_st data;
    long int msgtype = 0; //注意1

    //建立消息隊列
    msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
    if(msgid == -1)
    {
        fprintf(stderr, "msgget failed with error: %d\n", errno);
        exit(EXIT_FAILURE);
    }
    //從隊列中獲取消息,直到遇到end消息為止
    while(running)
    {
        if(msgrcv(msgid, (void*)&data, BUFSIZ, msgtype, 0) == -1)
        {
            fprintf(stderr, "msgrcv failed with errno: %d\n", errno);
            exit(EXIT_FAILURE);
        }
        printf("You wrote: %s\n",data.text);
        //遇到end結束
        if(strncmp(data.text, "end", 3) == 0)
            running = 0;
    }
    //刪除消息隊列
    if(msgctl(msgid, IPC_RMID, 0) == -1)
    {
        fprintf(stderr, "msgctl(IPC_RMID) failed\n");
        exit(EXIT_FAILURE);
    }
    exit(EXIT_SUCCESS);
}
 
發送者:
#define MAX_TEXT 512
struct msg_st
{
    long int msg_type;
    char text[MAX_TEXT];
};

int main()
{
    int running = 1;
    struct msg_st data;
    char buffer[BUFSIZ];
    int msgid = -1;

    //建立消息隊列
    msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
    if(msgid == -1)
    {
        fprintf(stderr, "msgget failed with error: %d\n", errno);
        exit(EXIT_FAILURE);
    }

    //向消息隊列中寫消息,直到寫入end
    while(running)
    {
        //輸入數據
        printf("Enter some text: ");
        fgets(buffer, BUFSIZ, stdin);
        data.msg_type = 1;    //注意2
        strcpy(data.text, buffer);
        //向隊列發送數據
        if(msgsnd(msgid, (void*)&data, MAX_TEXT, 0) == -1)
        {
            fprintf(stderr, "msgsnd failed\n");
            exit(EXIT_FAILURE);
        }
        //輸入end結束輸入
        if(strncmp(buffer, "end", 3) == 0)
            running = 0;
        sleep(1);
    }
    exit(EXIT_SUCCESS);
}
 

共享內存

不同進程之間共享的內存通常安排為同一段物理內存。進程可以將同一段共享內存連接到它們自己的地址空間中,所有進程都可以訪問共享內存中的地址。共享內存並未提供同步機制,也就是說,在第一個進程結束對共享內存的寫操作之前,並無自動機制可以阻止第二個進程開始對它進行讀取。所以我們通常需要用其他的機制來同步對共享內存的訪問,例如前面說到的信號量。
系統調用如下:
int shmget(key_t key, size_t size, int shmflg);
key為共享內存段命名,size以字節為單位指定需要共享的內存容量,shmflg是權限標志,它的作用與open函數的mode參數一樣,將其與IPC_CREAT做或操作表示共享內存不存在則創建。共享內存的權限標志與文件的讀寫權限一樣。
void *shmat(int shm_id, const void *shm_addr, int shmflg);
啟動對該共享內存的訪問,並把共享內存連接到當前進程的地址空間。shm_addr指定共享內存連接到當前進程中的地址位置,通常為空,表示讓系統來選擇共享內存的地址。shm_flg是一組標志位,通常為0。調用成功時返回一個指向共享內存第一個字節的指針,如果調用失敗返回-1.
int shmdt(const void *shmaddr);
將共享內存從當前進程中分離。
int shmctl(int shm_id, int command, struct shmid_ds *buf);和消息隊列含義相似。
讀進程:
int main()
{
    int running = 1;//程序是否繼續運行的標志
    void *shm = NULL;//分配的共享內存的原始首地址
    struct shared_use_st *shared;//指向shm
    int shmid;//共享內存標識符
    //創建共享內存
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if(shmid == -1)
    {
        fprintf(stderr, "shmget failed\n");
        exit(EXIT_FAILURE);
    }
    //將共享內存連接到當前進程的地址空間
    shm = shmat(shmid, 0, 0);
    if(shm == (void*)-1)
    {
        fprintf(stderr, "shmat failed\n");
        exit(EXIT_FAILURE);
    }
    printf("\nMemory attached at %X\n", (int)shm);
    //設置共享內存
    shared = (struct shared_use_st*)shm;
    shared->written = 0;
    while(running)//讀取共享內存中的數據
    {
        //沒有進程向共享內存定數據有數據可讀取
        if(shared->written != 0)
        {
            printf("You wrote: %s", shared->text);
            sleep(rand() % 3);
            //讀取完數據,設置written使共享內存段可寫
            shared->written = 0;
            //輸入了end,退出循環(程序)
            if(strncmp(shared->text, "end", 3) == 0)
                running = 0;
        }
        else//有其他進程在寫數據,不能讀取數據
            sleep(1);
    }
    //把共享內存從當前進程中分離
    if(shmdt(shm) == -1)
    {
        fprintf(stderr, "shmdt failed\n");
        exit(EXIT_FAILURE);
    }
    //刪除共享內存
    if(shmctl(shmid, IPC_RMID, 0) == -1)
    {
        fprintf(stderr, "shmctl(IPC_RMID) failed\n");
        exit(EXIT_FAILURE);
    }
    exit(EXIT_SUCCESS);
}
 
寫進程:
int main()
{
    int running = 1;
    void *shm = NULL;
    struct shared_use_st *shared = NULL;
    char buffer[BUFSIZ + 1];//用於保存輸入的文本
    int shmid;
    //創建共享內存
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if(shmid == -1)
    {
        fprintf(stderr, "shmget failed\n");
        exit(EXIT_FAILURE);
    }
    //將共享內存連接到當前進程的地址空間
    shm = shmat(shmid, (void*)0, 0);
    if(shm == (void*)-1)
    {
        fprintf(stderr, "shmat failed\n");
        exit(EXIT_FAILURE);
    }
    printf("Memory attached at %X\n", (int)shm);
    //設置共享內存
    shared = (struct shared_use_st*)shm;
    while(running)//向共享內存中寫數據
    {
        //數據還沒有被讀取,則等待數據被讀取,不能向共享內存中寫入文本
        while(shared->written == 1)
        {
            sleep(1);
            printf("Waiting...\n");
        }
        //向共享內存中寫入數據
        printf("Enter some text: ");
        fgets(buffer, BUFSIZ, stdin);
        strncpy(shared->text, buffer, TEXT_SZ);
        //寫完數據,設置written使共享內存段可讀
        shared->written = 1;
        //輸入了end,退出循環(程序)
        if(strncmp(buffer, "end", 3) == 0)
            running = 0;
    }
    //把共享內存從當前進程中分離
    if(shmdt(shm) == -1)
    {
        fprintf(stderr, "shmdt failed\n");
        exit(EXIT_FAILURE);
    }
    sleep(2);
    exit(EXIT_SUCCESS);
}
 

信號量

信號量用來協調對共享資源的操作,只允許對它進行等待(即P(信號變量))和發送(即V(信號變量))信息操作,且對信號量的操作都是原子操作。
系統調用:
int semget(key_t key, int num_sems, int sem_flags);
num_sems表示信號量數目。其它兩個參數和消息隊列的msgget函數參數含義相似。
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
改變信號量的值,sem_id是由semget返回的信號量標識符,sem_opa指向信號量集操作的數組。sembuf結構的定義如下:
struct sembuf{
short sem_num;//除非使用一組信號量,否則它為0
short sem_op;//信號量在一次操作中需要改變的數據,-1,即P(等待)操作+1,即V(發送信號)操作,0表示sleep直到信號量的值為0,即資源得到百分百利用。
short sem_flg;//通常為SEM_UNDO,使操作系統跟蹤信號,並在進程沒有釋放該信號量而終止時,操作系統釋放信號量;IPC_NOWAIT得不到資源立刻返回。
};
int semctl ( int semid, int semnum, int cmd, union semun arg );
控制信號量信息。semnum表示semid信號量集中的哪一個信號,cmd代表對信號的操作,
IPC_STAT/IPC_SET/IPC_RMID等。
/* arg for semctl system calls. */
union semun {
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */
ushort *array; /* array for GETALL & SETALL */
struct seminfo *__buf; /* buffer for IPC_INFO */
void *__pad;
};
兩個進程打印字符的實例,競爭打到控制台:
int main(int argc, char *argv[])
{
    char message = 'X';
    int i = 0;
    //創建信號量
    sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);
    if(argc > 1)
    {
        //程序第一次被調用,初始化信號量
        if(!set_semvalue())
        {
            fprintf(stderr, "Failed to initialize semaphore\n");
            exit(EXIT_FAILURE);
        }
        //設置要輸出到屏幕中的信息,即其參數的第一個字符
        message = argv[1][0];
        sleep(2);
    }
    for(i = 0; i < 10; ++i)
    {
        //進入臨界區
        if(!semaphore_p())
            exit(EXIT_FAILURE);
        //向屏幕中輸出數據
        printf("%c", message);
        //清理緩沖區,然后休眠隨機時間
        fflush(stdout);
        sleep(rand() % 3);
        //離開臨界區前再一次向屏幕輸出數據
        printf("%c", message);
        fflush(stdout);
        //離開臨界區,休眠隨機時間后繼續循環
        if(!semaphore_v())
            exit(EXIT_FAILURE);
        sleep(rand() % 2);
    }
    sleep(10);
    printf("\n%d - finished\n", getpid());
    if(argc > 1)
    {
        //如果程序是第一次被調用,則在退出前刪除信號量
        sleep(3);
        del_semvalue();
    }
    exit(EXIT_SUCCESS);
}
static int set_semvalue()
{
    //用於初始化信號量,在使用信號量前必須這樣做
    union semun sem_union;
    sem_union.val = 1;
    if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
        return 0;
    return 1;
}
static void del_semvalue()
{
    //刪除信號量
    union semun sem_union;
    if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
        fprintf(stderr, "Failed to delete semaphore\n");
}
static int semaphore_p()
{
    //對信號量做減1操作,即等待P(sv)
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = -1;//P()
    sem_b.sem_flg = SEM_UNDO;
    if(semop(sem_id, &sem_b, 1) == -1)
    {
        fprintf(stderr, "semaphore_p failed\n");
        return 0;
    }
    return 1;
}
static int semaphore_v()
{
    //這是一個釋放操作,它使信號量變為可用,即發送信號V(sv)
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = 1;//V()
    sem_b.sem_flg = SEM_UNDO;
    if(semop(sem_id, &sem_b, 1) == -1)
    {
        fprintf(stderr, "semaphore_v failed\n");
        return 0;
    }
    return 1;
}
 

各ipc方式比較

管道用於具有親緣關系的進程間通信,有名管道的每個管道具有名字,使沒有親緣關系的進程間也可以通信。
信號是比較復雜的通信方式,用於通知接受進程有某種事件發生,除了用於進程間通信外,進程還可以發送信號給進程本身。
消息隊列是消息的鏈接表,消息隊列克服了信號承載信息量少,管道只能承載無格式字節流以及緩沖區大小受限等缺點。
共享內存使得多個進程可以訪問同一塊內存空間,是最快的可用IPC形式,針對其他通信機制運行效率較低而設計的。往往與其它通信機制,如信號量結合使用,來達到進程間的同步及互斥。
信號量(semaphore)主要作為進程間以及同一進程不同線程之間的同步手段。
套接口(Socket)更為一般的進程間通信機制,可用於不同機器之間的進程間通信。
 

參考文獻

Linux Interprocess Communications.https://www.tldp.org/LDP/lpg/node7.html
Linux下的多進程通信.https://www.tianmaying.com/tutorial/LinuxIPC
Linux進程間通信——使用信號.https://blog.csdn.net/ljianhui/article/details/10128731
信號(上).https://www.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html
Linux進程間通信——使用消息隊列.https://blog.csdn.net/ljianhui/article/details/10287879
Linux進程間通信——使用共享內存.https://blog.csdn.net/ljianhui/article/details/10253345
Linux進程間通信——使用信號量.https://blog.csdn.net/ljianhui/article/details/10243617
Linux進程間通信——使用流套接字.https://blog.csdn.net/ljianhui/article/details/10477427
Linux進程間通信——使用數據報套接.https://blog.csdn.net/ljianhui/article/details/10697935
深刻理解Linux進程間通信(IPC).https://www.ibm.com/developerworks/cn/linux/l-ipc/index.html 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM