linux進程間/線程間通訊(《unix網絡編程-進程間通訊》讀書筆記)

linux進程間/線程間通訊

linux下的進程通信手段基本上是從Unix平臺上的進程通信手段繼承而來的。而對Unix發展做出重大貢獻的兩大主力AT&T的貝爾實驗室及BSD(加州大學伯克利分校的伯克利軟件發佈中心)在進程間通信方面的側重點有所不同。前者對Unix早期的進程間通信手段進行了系統的改進和擴充,形成了“system V IPC”,通信進程侷限在單個計算機內;後者則跳過了該限制,形成了基於套接口(socket)的進程間通信機制。Linux則把兩者繼承了下來,如圖示

Posix,全稱“Portable operating System Interface”,可移植操作系統接口。 是IEEE開發的一套編碼標準, 它已經是ISO/IEC採納的國際標準。

一般System V IPC,都需要 ftok() 使用路徑名生成key值, 而Posix IPC 直接使用路徑名,且Posix IPC接口函數裏面都有 "_" 連接符, 例如: mq_open/sem_open() 等。


linux下進程間通信的幾種主要手段簡介:

  1. 管道(Pipe)及有名管道(named pipe):管道可用於具有親緣關係進程間的通信,管道只能承載無格式字節流。
  2. 有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關係進程間的通信;
  3. 信號(Signal):信號是比較複雜的通信方式,用於通知接受進程有某種事件發生,除了用於進程間通信外,進程還可以發送信號給進程本身;linux除了支持Unix早期信號語義函數sigal外,還支持語義符合Posix.1標準的信號函數sigaction(實際上,該函數是基於BSD的,BSD爲了實現可靠信號機制,又能夠統一對外接口,用sigaction函數重新實現了signal函數);
  4. 報文(Message)隊列(消息隊列):消息隊列是消息的鏈接表,包括Posix消息隊列system V消息隊列。有足夠權限的進程可以向隊列中添加消息,被賦予讀權限的進程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節流以及緩衝區大小受限等缺點。
  5. 信號量(semaphore):主要作爲進程間以及同一進程不同線程之間的同步手段。
  6. 共享內存:使得多個進程可以訪問同一塊內存空間,是最快的可用IPC形式。是針對其他通信機制運行效率較低而設計的。往往與其它通信機制,如信號量結合使用,來達到進程間的同步及互斥。
  7. 套接口(Socket):更爲一般的進程間通信機制,可用於不同機器之間的進程間通信。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上:Linux和System V的變種都支持套接字。


1.管道

管道的主要侷限性正體現在它的特點上:

    只支持單向數據流;
    只能用於具有親緣關係的進程之間;
    沒有名字;
    管道的緩衝區是有限的(管道制存在於內存中,在管道創建時,爲緩衝區分配一個頁面大小);
    管道所傳送的是無格式字節流,這就要求管道的讀出方和寫入方必須事先約定好數據的格式,比如多少字節算作一個消息(或命令、或記錄)等等;

接口:
    pipe()/ fork()
    

2. 有名管道

    FIFO可以說是管道的推廣,克服了管道無名字的限制,使得無親緣關係的進程同樣可以採用先進先出的通信機制進行通信。
    
FIFO的打開規則:
如果當前打開操作是爲讀而打開FIFO時,若已經有相應進程爲寫而打開該FIFO,則當前打開操作將成功返回;否則,可能阻塞直到有相應進程爲寫而打開該FIFO(當前打開操作設置了阻塞標誌);或者,成功返回(當前打開操作沒有設置阻塞標誌)。
如果當前打開操作是爲寫而打開FIFO時,如果已經有相應進程爲讀而打開該FIFO,則當前打開操作將成功返回;否則,可能阻塞直到有相應進程爲讀而打開該FIFO(當前打開操作設置了阻塞標誌);或者,返回ENXIO錯誤(當前打開操作沒有設置阻塞標誌)。

注意點:
    不管寫打開的阻塞標誌是否設置,在請求寫入的字節數大於4096時,都不保證寫入的原子性。但二者有本質區別:
對於阻塞寫來說,寫操作在寫滿FIFO的空閒區域後,會一直等待,直到寫完所有數據爲止,請求寫入的數據最終都會寫入FIFO;

而非阻塞寫則在寫滿FIFO的空閒區域後,就返回(實際寫入的字節數),所以有些數據最終不能夠寫入。



接口:
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char * pathname, mode_t mode)              //pathname 是一個普通的unix路徑名,例如 /home/sundh/111.txt; 不同於,posix接口要求的路徑名裏面不能出現兩次“/” 符號。

int open(const char *path, int oflag, ...  );   //oflag 模式是BLOCK的, 如果顯示的加上O_NONBLOCK,則不會出現open()阻塞等待另外一個線程調用open()函數。

int close()

int unlink(const char *path);    //對FIFO文件進行刪除

區別:

有名管道是作爲一個特殊的設備文件存在於磁盤當中,而管道存在於內存當中,通信結束後,有名管道的文件本身依然存在(除非調用unlink()函數對fifo文件進行刪除),但是管道已經釋放了。

有名管道與文件也是有區別的,文件的話,當讀取其中的內容之後,信息依然存在,但是有名管道中,通信結束之後,信息就會丟失

3. 信號

kill -l 可以看到系統支持哪些信號。


信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一箇中斷請求可以說是一樣的。
信號是異步的,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什麼時候到達。
信號是進程間通信機制中唯一的異步通信機制,可以看作是異步通知,通知接收信號的進程有哪些事情發生了。

信號來源:
信號事件的發生有兩個來源:硬件來源(比如我們按下了鍵盤或者其它硬件故障);
軟件來源,最常用發送信號的系統函數是kill, raise, about, alarm和setitimer以及sigqueue函數,軟件來源還包括一些非法運算等操作。

信號值小於SIGRTMIN的信號都是不可靠信息,可能會丟失(不支持排隊,即不允許隊列中有多個相同的信息,例如如果進程隊列中有SIGINT,再來一個SIGINT消息,就不會插入到隊列中了);
信號值位於SIGRTMIN和SIGRTMAX之間的信號都是可靠信號,它們支持排隊,且允許隊列中有多個相同的信息。

函數接口:
 Linux在支持新版本的信號安裝函數sigation()以及信號發送函數sigqueue()的同時,仍然支持早期的signal()信號安裝函數,支持信號發送函數kill()。
 最常用發送信號的系統函數是kill, raise, about, alarm和setitimer以及sigqueue函數,軟件來源還包括一些非法運算等操作。
 

4. 消息隊列

4.1 System V

消息隊列就是一個消息的鏈表。可以把消息看作一個記錄,具有特定的格式以及特定的優先級。對消息隊列有寫權限的進程可以向中按照一定的規則添加新消息;

對消息隊列有讀權限的進程則可以從消息隊列中讀走消息。消息隊列是隨內核持續的。

函數接口:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok (char*pathname, char proj);    //convert a pathname and a project identifier to a System V IPC key. 如果pathname所指的文件不存在,則放回-1. msgget(-1, IPC_CREAT) 這個函數會隨機返回一個唯一值

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg)        //將創建一個新的消息隊列或取得一個已有信號量
int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg);  //讀取一個消息,並把消息存儲在msgp指向的msgbuf結構中。
int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg);     //向msgid代表的消息隊列發送一個消息,即將發送的消息存儲在msgp指向的msgbuf結構中,消息的大小由msgze指定。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);         //該系統調用對由msqid標識的消息隊列執行cmd操作,共有三種cmd操作:IPC_STAT、IPC_SET 、IPC_RMID。

 比較:
 消息隊列與管道以及有名管道相比,具有更大的靈活性,首先,它提供有格式字節流,有利於減少開發人員的工作量;其次,消息具有類型,在實際應用中,可作爲優先級使用。這兩點是管道以及有名管道所不能比的。

 同樣,消息隊列可以在幾個進程間複用,而不管這幾個進程是否具有親緣關係,這一點與有名管道很相似;但消息隊列是隨內核持續的,與有名管道(隨進程持續)相比,生命力更強,應用空間更大。

4.2 posix 消息隊列

mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);

//name 字符串格式有固定的要求 “/somename". 必須以/開始,somename裏面不允許再有/符號。

//POSIX 消息隊列生命期是內核生命期的;如果沒有使用mq_unlink(3) 刪除的話,一個消息隊列會一直存在,直到系統關閉。

//創建的消息隊列在一個虛擬的文件系統裏,我們可以把掛載這個虛擬的消息隊列文件系統在 Linux 系統中。 這樣就可以看到const char *name 文件夾。

# mkdir /dev/mqueue

# mount -t mqueue none /dev/mqueue

mq_open.c 代碼如下:

  1 #include <unistd.h>
  2 #include <fcntl.h>           /* For O_* constants */
  3 #include <sys/stat.h>        /* For mode constants */
  4 #include <mqueue.h>
  5 #include <stdio.h>
  6 #include <errno.h>
  7
  8 #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
  9
 10 int main(int argc, char**argv)
 11 {
 12         int c, flags;
 13         mqd_t mqd;
 14
 15         printf("*************argv[0]:%s, %s\n", argv[0], argv[1]);
 16         flags = O_RDWR|O_CREAT;
 17         while( (c=getopt(argc, argv, "e"))!= -1)
 18         {
 19         if(c =='e')
 20         {
 21         printf("have option e \n");
 22         flags = flags|O_EXCL;
 23         break;
 24         }
 25         break;
 26         }
 27
 28         mqd = mq_open(argv[optind], flags, FILE_MODE, NULL);
 29         printf("*************argv[optind]:%s, %d\n", argv[optind], optind);
 30         if(mqd == -1)
 31                 printf("create error \n");
 32         printf(" %d\n",  errno);
 33         perror("result:");
 34         mq_close(mqd);
 35         return 0;
 36
 37 }

運行這個可執行文件 ./mq_open /test, 執行 ls /dev/mqueue就可以在看到 test文件。

sundh@linux:~/temp$ ls /dev/mqueue
test
sundh@linux:~/temp$ cat /dev/mqueue/test
QSIZE:0          NOTIFY:0     SIGNO:0     NOTIFY_PID:0

int mq_close(mqd_t mqdes);

int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio);

ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned *msg_prio);

int mq_unlink(const char *name);

5. 信號量

5.1 Posix

Posix 信號量分爲兩種: 有名信號量和基於內存的信號量(也就是無名信號量)。

先看有名信號量:

sem_t *sem_open(const char *name, int oflag); ://creates a new POSIX semaphore or opens an existingsemaphore. 
創建一個有名的信號量,它可以用於線程也可以用於進程間的同步。name變量不同於ftok (char*pathname, …)中的pathname, 只要兩個進程的name字符串是一致且只有第一個字符是/,不能出現兩個/字符,就可以使用這個信號量進行同步。
 (對於POSIX信號量和共享內存的name參數,會在/dev/shm下建立對應的路徑名。 可參考 http://blog.csdn.net/anonymalias/article/details/9938865)

sem_close() : 只是關閉信號量,並未從系統中刪除

sem_unlink(): 刪除該信號量

sem_close() 和 sem_unlink()並不是一定要先調用sem_close(),然後調用sem_unlink(), 沒有這樣的調用要求.  可以先sem_unlink()從系統內核中刪除該信號量的名字,既從/dev/shm 中刪除該信號量對應的文件, 然後還能調用sem_post() 和sem_wait()進行信號量操作,最後調用 sem_close().   可參考 後面第6章的例子1。


《unix網絡編程-進程間通信》這樣解釋sem_close() 和 sem_unlink():



sem_close()不是強制要求調用的,進程退出時,如果有信號量被這個進程打開,沒有被關閉,退出時會自動關閉該信號量。

sem_wait()/sem_trywait():當所指定的信號量的值爲0時,後者並不將調用者投入睡眠,而是立刻返回EAGAIN,即重試。

按照功能來分有兩種, 二進制信號量和記數信號量。 


//process 1
#include <iostream>
#include <cstring>
#include <errno.h>


#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>


using namespace std;


#define SHM_NAME "/memmap"
#define SHM_NAME_SEM "/memmap_sem" 


char sharedMem[10];


int main()
{
    int fd;
    sem_t *sem;


    fd = shm_open(SHM_NAME, O_RDWR | O_CREAT, 0666);
    sem = sem_open(SHM_NAME_SEM, O_CREAT, 0666, 0);


    if (fd < 0 || sem == SEM_FAILED)
    {
        cout<<"shm_open or sem_open failed...";
        cout<<strerror(errno)<<endl;
        return -1;
    }


    ftruncate(fd, sizeof(sharedMem));


    char *memPtr;
    memPtr = (char *)mmap(NULL, sizeof(sharedMem), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);


    char msg[] = "yuki...";


    memmove(memPtr, msg, sizeof(msg));
    cout<<"process:"<<getpid()<<" send:"<<memPtr<<endl;


    sem_post(sem);
    sem_close(sem);


    return 0;
}


//process 2
#include <iostream>
#include <cstring>
#include <errno.h>


#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>


using namespace std;


#define SHM_NAME "/memmap"
#define SHM_NAME_SEM "/memmap_sem" 


int main()
{
    int fd;
    sem_t *sem;


    fd = shm_open(SHM_NAME, O_RDWR, 0);
    sem = sem_open(SHM_NAME_SEM, 0);


    if (fd < 0 || sem == SEM_FAILED)
    {
        cout<<"shm_open or sem_open failed...";
        cout<<strerror(errno)<<endl;
        return -1;
    }


    struct stat fileStat;
    fstat(fd, &fileStat);


    char *memPtr;
    memPtr = (char *)mmap(NULL, fileStat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);


    sem_wait(sem);


    cout<<"process:"<<getpid()<<" recv:"<<memPtr<<endl;


    sem_close(sem);


    return 0;
}

程序執行結果:

# ./send 
process:13719 send:yuki...
# ./recv 
process:13720 recv:yuki...

對於POSIX信號量和共享內存的名字會在/dev/shm下建立對應的路徑名,例如上面的測試代碼,會生成如下的路徑名:

# ll /dev/shm/
total 8
-rw-r--r-- 1 root root 10 Aug 13 00:28 memmap
-rw-r--r-- 1 root root 32 Aug 13 00:28 sem.memmap_sem

5.1.1 有名信號量

sem_open() 用於創建一個新的有名信號量或者打開一個已經存在的有名信號量,它可用於線程間同步或者進程間同步。 有名信號量是由內核維護的。不需要程序分配信號量的內存空間。

一般主線程或者主進程調用 sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value) 來創建一個新的信號量, mode_t mode 設置爲O_CREAT。另外一個線程或者經常使用sem_t *sem_open(const char *name, int oflag)來打開一個已經存在的信號量。

5.1.2 無名信號量

即基於內存的信號量,隨進程結束也釋放。 它是有進程分配信號量的內存空間,然後調用sem_init初始化。

sem_t temp;    //如果這個temp是分配在共享內存區,則可以用於進程間通信;否則只能用於線程間通信。

int sem_init(sem_t *sem, int pshared, unsigned int value); //pshared爲0, 線程間通信; 不爲0時,sem_t temp必須在共享內存分配內存空間。

int sem_destroy(sem_t *sem);

很少有人會使用無名信號量用於進程間通信,所以也就不細說了。 用的較多的方法是先聲明一個結構體:

 struct shareMemory {    sem_t  sem;    char *buffer[MAX]};   在共享內存區上面分配一個這樣大小的內存,然後調用 sem_init(&shareMemory.sem, 1, 1).  這裏需要注意的是sem_init()只能被調用一次。對一個已經初始化的信號量調用sem_init(),其結果是未定義的。  也就是說 主線程或者主進程調用 sem_init()後,另外的線程或者進程不允許再次調用sem_init(),這個另外的線程或者進程可以直接使用 shareMemory.sem進行同步。



如果pshared 的值爲0,並且sem_t 這個信號量是對所有線程都可見,例如:聲明爲全局變量,或者用new/malloc 分配的堆內存,或者信號量是局部變量,通過需要通過pthread_create(pthread_t* thread, pthread_attr_t, void *(*Func), void *arg)函數中的 void* arg形參在創建新線程的時候傳入,這時這個信號量就可以線程間同步了。

  1. /* 
  2.  * simple_sem_app.c 
  3.  */  
  4. #include "all.h"  
  5.   
  6. /* 每個字符輸出的間隔時間 */  
  7. #define TEN_MILLION 5000000L  
  8. #define BUFSIZE 1024  
  9.   
  10. void *threadout(void *args);  
  11.   
  12. int main(int argc, char *argv[])  
  13. {  
  14.     int error;  
  15.        int i;  
  16.        int n;  
  17.     sem_t semlock;  
  18.        pthread_t *tids;  
  19.      
  20.        if (argc != 2) {  
  21.            fprintf (stderr, "Usage: %s numthreads\n", argv[0]);  
  22.               return 1;  
  23.        }    
  24.        n = atoi(argv[1]);  
  25.        tids = (pthread_t *)calloc(n, sizeof(pthread_t));  
  26.        if (tids == NULL) {  
  27.            perror("Failed to allocate memory for thread IDs");  
  28.            return 1;  
  29.        }    
  30.        if (sem_init(&semlock, 0, 1) == -1) {  
  31.            perror("Failed to initialize semaphore");  
  32.            return 1;  
  33.        }    
  34.        for (i = 0; i < n; i++) {  
  35.            if (error = pthread_create(tids + i, NULL, threadout, &semlock)) {  
  36.                fprintf(stderr, "Failed to create thread:%s\n", strerror(error));  
  37.                   return 1;  
  38.           }  
  39.     }  
  40.        for (i = 0; i < n; i++) {  
  41.            if (error = pthread_join(tids[i], NULL)) {  
  42.                fprintf(stderr, "Failed to join thread:%s\n", strerror(error));  
  43.                  return 1;  
  44.               }  
  45.     }  
  46.     return 0;  
  47. }  
  48.   
  49. void *threadout(void *args)  
  50. {  
  51.     char buffer[BUFSIZE];  
  52.        char *c;  
  53.        sem_t *semlockp;  
  54.        struct timespec sleeptime;  
  55.      
  56.        semlockp = (sem_t *)args;  
  57.        sleeptime.tv_sec = 0;  
  58.        sleeptime.tv_nsec = TEN_MILLION;  
  59.      
  60.        snprintf(buffer, BUFSIZE, "This is thread from process %ld\n",  
  61.                (long)getpid());  
  62.        c = buffer;  
  63.        /****************** entry section *******************************/  
  64.        while (sem_wait(semlockp) == -1)  
  65.            if(errno != EINTR) {  
  66.                fprintf(stderr, "Thread failed to lock semaphore\n");  
  67.                  return NULL;  
  68.               }  
  69.        /****************** start of critical section *******************/  
  70.        while (*c != '\0') {  
  71.               fputc(*c, stderr);  
  72.               c++;  
  73.               nanosleep(&sleeptime, NULL);  
  74.        }  
  75.        /****************** exit section ********************************/  
  76.        if (sem_post(semlockp) == -1)  
  77.               fprintf(stderr, "Thread failed to unlock semaphore\n");  
  78.        /****************** remainder section ***************************/  
  79.        return NULL;  

5.1.3二進制信號量

可用於互斥目的,就像互斥量一樣。它除了像互斥量那樣使用外,還有一個互斥量沒有的特性:互斥量必須總是由鎖住它的線程解鎖,二進制信號量的sem_wait() 和 sem_post()可處於不同的線程或者進程。


條件變量 和 信號量的區別:

1. 條件變量一般用於線程間的同步; 信號量一般用於進程間同步, 當然信號量也可以用於線程間同步。  條件變量也可以用於進程間同步,不過這種用法使用的比較少,有特定要求,就是要在共享內存上面分配條件變量及其關聯的mutex互斥量的內存,不推薦這條件變量用於進程間同步。

2. 信號量執行 sem_post()函數後,這個信號量的值總是 >0 ,它會一直等到 用戶調用sem_wait() 來把信號量的值減1.   但條件變量不是這樣,如果執行pthread_cond_signal()之前,沒有線程執行 pthread_cond_wait(), 那這個signal會被丟失。


5.2 System v

這種信號量機制用的比較少,已經被淘汰了。
    
函數接口(Sytem V):
#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok (char*pathname, char proj);//convert a pathname and a project identifier to a System V IPC key, 如果pathname所指的文件不存在,則放回-1. semget(-1, IPC_CREAT) 這個函數會隨機返回一個唯一值


int semget(key_t key, int num_sems, int sem_flags); 

//semget()創建一個新信號量集或取得一個已有信號量集, 第一個參數key是整數值(唯一非零),不相關的進程可以通過它訪問一個信號量。 key的值可以爲IPC_PRIVATE,這時這個信號量集用於線程間同步。  這個函數返回的是信號量標識符, semop() 和 semctl()能使用它。信號量標識符是int型,指向一個結構體地址,圖如下:


struct semid_ds{} 是一個信號量集結構體,sem_base是一個指針,指向一組信號量。  sem_nsems 標識信號量集中有多少個信號量。

semget() 函數中形參 int num_sems指定集合中信號量個數。 如果我們不創建一個新的信號量集,而只是訪問一個已存在的集合,那就可以把該參數指定爲0,也可指定爲已存在集合的信號量個數. 

semget() 僅僅只是創建一個信號量集,不進行初始化。 使用semctl(semid, 0, SETVAL, arg) 或者semctl(semid, 0,SETALL, arg) 進行初始化。


int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);     //改變信號量的值。
int semctl(int semid,int semnum,int cmd,union semun arg);        //該函數用來直接控制信號量信息

注意:
a.) semget()使用時,有一個特殊的信號量key值,IPC_PRIVATE(通常爲0),其作用是創建一個只有創建進程可以訪問的信號量,可以在線程間使用,不能在進程間使用。
(某些Linux系統上,手冊頁將IPC_PRIVATE並沒有阻止其他的進程訪問信號量作爲一個bug列出。)
b.)  在semget() 創建一個信號量後,需要使用 semctl( SETVAL) 來初始化這個信號量。

6. 共享內存

共享內存可以說是最有用的進程間通信方式,也是最快的IPC形式,不再涉及內核。兩個不同進程A、B共享內存的意思是,同一塊物理內存被映射到進程A、B各自的進程地址空間。進程A可以即時看到進程B對共享內存中數據的更新,反之亦然。由於多個進程共享同一塊內存區域,必然需要某種同步機制,互斥鎖和信號量都可以。   

Linux的2.2.x內核支持多種共享內存方式,

                 - mmap()系統調用

                 - Posix共享內存

                 - 系統V共享內存

6.1  mmap()系統調用
    mmap()函數把一個文件或者posix共享內存區對象映射到調用進程的地址空間。使用mmap函數的主要目的是:
        (1)對普通文件提供內存映射I/O,可以提供無親緣進程間的通信;
        (2)提供匿名內存映射,以供親緣進程間進行通信。
        (3)對shm_open創建的POSIX共享內存區對象進程內存映射,以供無親緣進程間進行通信。

(題外話: 一個12G的文件,電腦內存只有4G,該如何讀取這個文件的data?  理想情況可以這樣: 

int fd=open("data.txt", O_RDONLY); ptr = mmap(NULL, 4G,PORT_READ, MAP_SHARED, fd, 0); //映射文件0~4G數據到內存

ptr = mmap(NULL, 4G,PORT_READ, MAP_SHARED, fd, 4G); //映射文件4G~8G數據到內存

ptr = mmap(NULL, 4G,PORT_READ, MAP_SHARED, fd, 8G); //映射文件8G~12G數據到內存)
        
函數:
    void *mmap(void *start, size_t len, int prot, int flags, int fd, off_t offset);
參數fd爲即將映射到進程空間的文件描述字,一般由open()返回. 

flags 可以爲 MAP_SHARE 或 MAP_PRIVATE, 如果指定爲MAP_PRIVATE,那麼調用進程對映射數據所作的修改只對該進程可見,而不改變其底層支撐對象(支撐對象要麼是普通文件,要麼是匿名內存映射,要麼是shm_open創建的共享內存區)。 通俗的理解就是,如果共享對象是一個文件,那該進程往這個文件寫數據,別的進程都看不到,相當於線程間共享數據。 除此以外,flags爲MAP_PRIVATE時,munmap()刪除共享內存時,調用進程對這個共享內存所做的修改的都會被丟棄掉。

如果flags爲MAP_SHARE,則調用進程對共享內存區的修改對所有進程都可見。如果flags爲MAP_ANONYMOUS ,它表示匿名映射,映射區不與任何文件關聯。

同時,fd可以指定爲-1,此時須指定flags參數中的MAP_SHARED|MAP_ANONYMOUS,表明進行的是匿名映射(不涉及具體的文件名,避免了文件的創建及打開,很顯然只能用於具有親緣關係的進程間通信)。由於父子進程特殊的親緣關係,在父進程中先調用mmap(),然後調用 fork()。那麼在調用fork()之後,子進程繼承父進程匿名映射後的地址空間,同樣也繼承mmap()返回的地址,這樣,父子進程就可以通過映射區 域進行通信了。
    
    int munmap(void *start, size_t len);  
    int msync(void *start, size_t len, int flags);     //如果我們修改了處於內存共享區中某個位置的內容,那麼內核將在稍後的某個時刻更新相應的文件,如果我們希望數據立刻同步更新到文件,則調用這個函數。

並不是所有的文件都能進行內存映射。 例如一個訪問終端或者套接字的描述符是不能進行mmap映射的,它們只能使用read和write來訪問。
舉例:

int fd=open("/home/sunny/1.log",O_CREAT|O_RDWR|O_TRUNC,00777);
char * memPtr = (char *)mmap(NULL, sizeof(sharedMem), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 
或者: char * p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
       MAP_SHARED|MAP_ANONYMOUS,-1,0); 

例子1:



6.2 Posix共享內存

POSIX共享內存建立在mmap函數之上,我們首先指定待打開共享內存區的POSIXIPC名字來調用shm_open(),取得一個描述符後使用mmap函數把它映射到當前進程的內存空間!POSIX共享內存使用方法有以下兩個步驟:
    -  通過shm_open創建或打開一個POSIX共享內存對象;
    -  然後調用mmap將它映射到當前進程的地址空間;

函數:
    int shm_open(const char *name, int oflag, mode_t mode);   //shm_open用於創建一個新的共享內存區對象或打開一個已經存在的共享內存區對象。 這裏的name 和

sem_t *sem_open(const char *name, int oflag);中的 name 是一樣的。 只要兩個進程中的字符串一樣,不管裏面的值,例如:"/tmp/log.txt" (不管是否真實的存在這個文件),
這兩個進程就能共享。 因爲系統會在 /dev/shm下建立對應的路徑名

    int shm_unlink(const char *name);

    int ftruncate(int fd, off_t length); // 修改 fd指向的普通文件或者共享內存區對象的大小。

    int fstat(int fd, struct stat *bug);   //獲取fd該對象的信息

例子參考: http://blog.csdn.net/anonymalias/article/details/9938865

//process 1
#include <iostream>
#include <cstring>
#include <errno.h>

#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>

using namespace std;

#define SHM_NAME "/memmap"
#define SHM_NAME_SEM "/memmap_sem"

char sharedMem[10];

int main()
{
    int fd;
    sem_t *sem;

    fd = shm_open(SHM_NAME, O_RDWR | O_CREAT, 0666);
    sem = sem_open(SHM_NAME_SEM, O_CREAT, 0666, 0);

    if (fd < 0 || sem == SEM_FAILED)
    {
        cout<<"shm_open or sem_open failed...";
        cout<<strerror(errno)<<endl;
        return -1;
    }

    ftruncate(fd, sizeof(sharedMem));

    char *memPtr;
    memPtr = (char *)mmap(NULL, sizeof(sharedMem), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    char msg[] = "yuki...";

    memmove(memPtr, msg, sizeof(msg));
    cout<<"process:"<<getpid()<<" send:"<<memPtr<<endl;

    sem_post(sem);
    sem_close(sem);

    return 0;
}

//process 2
#include <iostream>
#include <cstring>
#include <errno.h>

#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>

using namespace std;

#define SHM_NAME "/memmap"
#define SHM_NAME_SEM "/memmap_sem"

int main()
{
    int fd;
    sem_t *sem;

    fd = shm_open(SHM_NAME, O_RDWR, 0);
    sem = sem_open(SHM_NAME_SEM, 0);

    if (fd < 0 || sem == SEM_FAILED)
    {
        cout<<"shm_open or sem_open failed...";
        cout<<strerror(errno)<<endl;
        return -1;
    }

    struct stat fileStat;
    fstat(fd, &fileStat);

    char *memPtr;
    memPtr = (char *)mmap(NULL, fileStat.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);

    sem_wait(sem);

    cout<<"process:"<<getpid()<<" recv:"<<memPtr<<endl;

    sem_close(sem);

    return 0;}


切換到 /dev/shm 目錄下,執行 ll 命令:

  1. # ll /dev/shm/  
  2. total 8  
  3. -rw-r--r-- 1 root root 10 Aug 13 00:28 memmap  
  4. -rw-r--r-- 1 root root 32 Aug 13 00:28 sem.memmap_sem 

3. 系統V共享內存

系統V則是通過映射特殊文件系統shm中的文件實現進程間的共享內存通信。 

函數接口: 

key_t ftok(const char *pathname, int proj_id);

int shmget(key_t key, size_t size, int shmflg);

shmat()、shmdt()及shmctl()


總結:

內存映射的同步:
  一般來說,進程在映射空間中對共享內容的修改並不會直接寫回到磁盤文件中,往往在調用munmap()之後纔會同步輸出到磁盤文件中.那麼,在程序運行過程中,在調用munmap()之前,可以通過調用msync()來實現磁盤上文件內容與共享內存區中的內容與一致;或者是把對共享內存區的修改同步輸出到磁盤文件中;
注意:
1、最終被映射文件內容的長度不會超過文件本身的初始大小,即:內存映射操作不能改變文件的大小;
2、可以用於進程間通信的得有效地址空間大小大體上受限於被映射文件的大小,但是並不完全受限於文件大小.
  在Linux中,內存的保護機制是以內存頁爲單位的,即使被映射的文件只有一個字節的大小,內核也會爲這個文件的映射分配一個頁面大小的內存空間.當被映射文件的大小小於一個頁面大小時,進程可以對mmap()返回地址開始的一個頁面大小進行訪問,而不會出錯;但是,如果對一個頁面之外的地址空間進行訪問,則導致錯誤發生.因此,可用於進程間通信的有效地址空間的大小不會超過被映射文件大小與一個頁面大小的和;
3、文件一旦被映射之後,調用mmap()的進程對返回地址空間的訪問就是對某一內存區域進行訪問,暫時脫離了磁盤上文件的影響.所有對mmap()返回地址空間的操作只在內存範圍內有意義,只有在調用了munmap()或msync()之後,纔會把映射內存中的相應內容寫回到磁盤文件中,所寫內容的大小仍然不會超過被映射文件的大小;
對mmap()返回的地址空間的訪問:
Linux採用的是頁式管理機制.對於用mmap()映射普通文件來說,進程會在自己的地址空間中新增加一塊空間,空間的大小由mmap()的len參數指定,注意:進程並不一定能夠對新增加的全部空間都進行有效的訪問.進程能夠訪問的有效地址空間的大小取決於文件中被映射部分的大小.簡單地說,能夠容納文件中被映射部分大小的最少頁面個數決定了進程從mmap()返回的地址開始,能夠訪問的有效地址空間大小.超過這個空間大小,內核會根據超過的嚴重程度返回發送不同的信號給進程.
注意:決定進程能夠訪問的有效地址空間大小的因素是文件中被映射的部分,而不是整個文件;另外,如果指定了文件的偏移部分,一定要注意爲頁面大小的整數倍;

總之:採用內存映射機制mmap()來實現進程間通信是很方便的,在應用層上,調用接口非常簡單,內部實現機制涉及到了Linux的存儲管理以及文件系統等方面的內用;


7. socket通訊

套接口編程的幾個重要步驟:
1. int socket( int domain, int type, int ptotocol);
創建套接口,參數domain指明通信域,如PF_UNIX(unix域),PF_INET(IPv4),PF_INET6(IPv6)等;type指明通信類型,如SOCK_STREAM(面向連接方式)、SOCK_DGRAM(非面向連接方式)等。一般來說,參數protocol可設置爲0,除非用在原始套接口上(原始套接口有一些特殊功能,後面還將介紹)。
int bind( int sockfd, const struct sockaddr * my_addr, socklen_t my_addr_len)
對於使用TCP傳輸協議通信方式來說,通信雙方需要給自己綁定一個唯一標識自己的套接口,以便建立連接;對於使用UDP傳輸協議,只需要服務器綁定一個標識自己的套接口就可以了,用戶則不需要綁定(在需要時,如調用connect時[注1],內核會自動分配一個本地地址和本地端口號)。綁定操作由系統調用bind()完成:
int connect( int sockfd, const struct sockaddr * servaddr, socklen_t addrlen)
int accept( int sockfd, struct sockaddr * cliaddr, socklen_t * addrlen)

常用的從套接口中接收數據的調用有:recv、recvfrom、recvmsg等,常用的向套接口中發送數據的調用有send、sendto、sendms


例子: 典型的TCP客戶代碼

... ...
int socket_fd;
struct sockaddr_in serv_addr ;
... ...
socket_fd = socket ( PF_INET, SOCK_STREAM, 0 );
bzero( &serv_addr, sizeof(serv_addr) );
serv_addr.sin_family = AF_INET ;  /* 指明通信協議族 */
serv_addr.sin_port = htons( 49152 ) ;       /* 分配端口號 */
inet_pton(AF_INET, " 192.168.0.11", &serv_addr.sin_sddr) ;
/* 分配地址,把點分十進制IPv4地址轉化爲32位二進制Ipv4地址。 */
connect( socket_fd, (struct sockaddr*)serv_addr, sizeof( serv_addr ) ) ; /* 向服務器發起連接請求 */
... ...							/* 發送和接收數據 */
... ...

 

 

linux線程間通信

1.  鎖機制:包括互斥鎖、條件變量、讀寫鎖

   1.1 互斥鎖提供了以排他方式防止數據結構被併發修改的方法。

   1.2 使用條件變量可以以原子的方式阻塞進程,直到某個特定條件爲真爲止。對條件的測試是在互斥鎖的保護下進行的。條件變量始終與互斥鎖一起使用。

   1.3 讀寫鎖允許多個線程同時讀共享數據,而對寫操作是互斥的。

2 信號量機制(Semaphore):包括無名線程信號量和命名線程信號量

3 信號機制(Signal):類似進程間的信號處理

4 全局變量(靜態變量也可以)來進行通信

linux 線程間同步:

1. mutex 互斥量:

用於保護臨界區,以保證任何時刻只有一個線程在執行臨界區的代碼。   如果mutex在共享內存上面分配內存,則可用於進程間同步,保證任何時刻只有個一個進程在執行臨界區的代碼。

1.1  如何初始化?

 1.1.1  如果互斥量變量是靜態分配的, 我們可以把它初始化爲常量: PTHREAD_MUTEX_INITIALIZER。   

               pthread_mutex_t   lock = PTHREAD_MUTEX_INITIALIZER;    如果pthread_mutex_t   lock 是全局變量, 那對所有線程都是可見的。 如果pthread_mutex_t   lock是局部變量或者是static局部變量,那僅僅只對創建這個互斥量的線程可見,對別的線程不可見。  需要通過pthread_create(pthread_t* thread, pthread_attr_t, void *(*Func), void *arg)函數中的 void* arg形參在創建新線程的時候傳入。

1.1.2  如果互斥鎖是動態分配的(例如調用 new 或者malloc) 或者分配在共享內存區中,則我們在運行之前需要調用 pthread_mutex_init() 來初始化它。 雖然不用pthread_mutex_init() 來初始化,程序也能正常運行, 但不建議這樣.

2. 條件變量:

條件變量用於等待。 每個條件變量總是有一個互斥量與之關聯。

              #include <pthread.h>

             int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);

             int pthread_cond_signal(pthread_cond_t *cptr);

             int pthread_cond_init(pthread_cond_t *cptr, pthread_condattr_t *attr);

             int pthread_cond_destory(pthread_cond_t *cptr);

             int pthread_mutex_init(pthread_mutex_t *cptr, pthread_mutexattr_t *attr);            

             int pthread_mutex_destory(pthread_mutex_t *cptr);

             int pthread_mutexattr_init(pthread_mutexattr_t *attr);            
             int pthread_mutexattr_destory(pthread_mutexattr_t *cptr);

             int pthread_condattr_init( pthread_condattr_t *attr);

             int pthread_condattr_destory(pthread_condattr_t *cptr);

             int pthread_mutexattr_getshared(pthread_mutexattr_t *attr, int *valptr);            
             int pthread_mutexattr_setshared(pthread_mutexattr_t *cptr, int value);   //value 值爲PTHREAD_PROCESS_SHARED,並且mutex變量是在共享內存上面分配地址的,則進程間同步。

             int pthread_condattr_getshared( pthread_condattr_t *attr, int *valptr);

             int pthread_condattr_setshared(pthread_condattr_t *cptr, int value); //value 值爲PTHREAD_PROCESS_SHARED,並且條件變量是在共享內存上面分配地址的,則進程間同步。

2.1  如何初始化?(與pthread_mutex_t 類似)

 2.1.1  如果條件變量是靜態分配的, 我們可以把它初始化爲常量: PTHREAD_COND_INITIALIZER。

              struct _COND {    pthread_mutex_t   lock = PTHREAD_MUTEX_INITIALIZER;    //條件變量需要用戶主動定義互斥量變量。

               pthread_cond_t   cond = PTHREAD_COND_INITIALIZER; };

   如果pthread_cond_t   cond/pthread_mutex_t   lock 是全局變量, 那對所有線程都是可見的。 如果pthread_cond_t   cond/pthread_mutex_t   lock是局部變量或者是static局部變量,那僅僅只對創建這個互斥量的線程可見,對別的線程不可見。  需要通過pthread_create(pthread_t* thread, pthread_attr_t, void *(*Func), void *arg)函數中的 void* arg形參在創建新線程的時候傳入。

2.1.2  如果pthread_cond_t   cond/pthread_mutex_t   lock是動態分配的(例如調用 new 或者malloc) 或者分配在共享內存區中,則我們在運行之前需要調用 pthread_cond_init()/pthread_mutex_init() 來初始化它。 雖然不用pthread_cond_init()/pthread_mutex_init() 來初始化,程序也能正常運行,但不建議這樣.

2.2 代碼使用方法:

線程一:

     struct _COND {    pthread_mutex_t   lock;    //條件變量需要用戶主動定義互斥量變量。
               pthread_cond_t   cond ;  bool bCondition } var = {PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, false;};

      pthread_mutex_lock(&var.lock);

      ........// 臨界區代碼

      bCondition = true;

      pthread_mutex_unlock(&var.lock);

      pthread_cond_signal(&var.cond);


線程二:

            pthread_mutex_lock(&var.lock);

            while(bCondition == false)

                   pthread_cond_wait(&var.cond, &var.lock);

             bCondition = false;

             pthread_mutex_unlock(&var.lock);


條件變量 和 信號量的區別:

1. 條件變量一般用於線程間的同步; 信號量一般用於進程間同步, 當然信號量也可以用於線程間同步。  條件變量也可以用於進程間同步,不過這種用法使用的比較少,有特定要求,就是要在共享內存上面分配條件變量及其關聯的mutex互斥量的內存,不推薦這條件變量用於進程間同步。

2. 信號量執行 sem_post()函數後,這個信號量的值總是 >0 ,它會一直等到 用戶調用sem_wait() 來把信號量的值減1.   但條件變量不是這樣,如果執行pthread_cond_signal()之前,沒有線程執行 pthread_cond_wait(), 那這個signal會被丟失。

 

線程間的通信目的主要是用於線程同步。所以線程沒有像進程通信中的用於數據交換的通信機制。

 

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章