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会被丢失。

线程间的通信目的主要是用于线程同步。所以线程没有像进程通信中的用于数据交换的通信机制。


猜你喜欢

转载自blog.csdn.net/sunny04/article/details/18084309
今日推荐