1.管道

管道其实就是内核里的一块缓存区域,由于一切皆文件的思想,其表现为文件。

管道又分为匿名管道和有名管道。

  1. int pipe(int fd[2]);
  2. int mkfifo(char *path,mode_t mode);
  3. int mkfifoat(int fd,char *path,mode_t mode);

首先谈论一下匿名管道,很明显,由于匿名管道无法和文件描述符或文件相关联,匿名管道的方式只能用于父子进程中。这是由于子进程可以得到父进程的副本,当然也包括对应其管道的缓存区域的文件描述符。

匿名管道会返回得到两个文件描述符,其中进程间通信 - 图1为读入端,进程间通信 - 图2为写入端。管道是半双工的,为了避免混乱,一般写入的进程需要关闭读入端,接受的进程需要关闭写入端。若要满足全双工,则需要创建两个管道。

  1. int fd[2];
  2. pid_t pid;
  3. char line[MAXLINE];
  4. if(pipe(fd) < 0){
  5. err_sys("pipe error");
  6. }
  7. if((pid = fork()) < 0){
  8. err_sys("fork error");
  9. }else if(pid > 0){ // parent
  10. close(fd[0]);
  11. write(fd[1],"hello world\n",12);
  12. }else{ // child
  13. close(fd[1]);
  14. n = read(fd[0],line,MAXLINE);
  15. write(STDOUT_FILENO,line,n);
  16. }
  • 优点:简单易用。
  • 缺点:1)只能创建在它的进程以及其有亲缘关系的进程之间。
        2)管道缓冲区有限。

有名管道由于与文件/文件描述符关联,因此可以在无亲缘关系的进程中使用。

  • 优点:可以实现任意关系的进程间的通信;
  • 缺点:1)长期存于系统中,使用不当容易出错。
        2)缓冲区有限。

2.内存映射

内存映射是将磁盘数据映射到内存,在内存中进行操作的变化会体现在磁盘中。

原理是如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。

注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。

  1. #include <sys/mman.h>
  2. void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
  3. /*
  4. - 功能:将一个文件或者设备的数据映射到内存中
  5. - 参数:
  6. - void *addr: 指定NULL时, 由内核指定分配
  7. - length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
  8. 获取文件的长度:stat lseek(fd,0,SEEK_END)
  9. - prot : 对申请的内存映射区的操作权限
  10. -PROT_EXEC :可执行的权限
  11. -PROT_READ :读权限
  12. -PROT_WRITE :写权限
  13. -PROT_NONE :没有权限
  14. 要操作映射内存,必须要有读的权限。
  15. PROT_READ、PROT_READ|PROT_WRITE
  16. - flags :
  17. - MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
  18. - MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
  19. - fd: 需要映射的那个文件的文件描述符
  20. - 通过open得到,open的是一个磁盘文件
  21. - 注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。
  22. prot: PROT_READ open:只读/读写
  23. prot: PROT_READ | PROT_WRITE open:读写
  24. - offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不便宜。
  25. - 返回值:返回创建的内存的首地址
  26. 失败返回MAP_FAILED,即是(void *) -1
  27. */
  28. int munmap(void *addr, size_t length);
  29. /*
  30. - 功能:释放内存映射
  31. - 参数:
  32. - addr : 要释放的内存的首地址
  33. - length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
  34. */
  1. int fd = open("test.txt",O_RDWR);
  2. if(fd <= 0){
  3. err_sys("open");
  4. exit(0);
  5. }
  6. int len = lseek(fd,0,SEEK_END);
  7. void * addr = mmap(NULL,len,PORT_READ|PORT_WRITE,MAP_SHARED,fd,0);
  8. if(addr == MAP_FAILED){
  9. perror("mmap");
  10. exit(0);
  11. }
  12. strcpy(addr,"Write to file!\n");
  13. munmap(addr,len);

内存映射和常见文件I/O区别

常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。

而使用进程间通信 - 图3操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。进程间通信 - 图4的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此进程间通信 - 图5效率更高。

3.XSI IPC

有三种称作进程间通信 - 图6 进程间通信 - 图7进程间通信 - 图8:消息队列、信号量和共享存储器。

每个内核的IPC都用一个非负整数的标识符引用,消息队列为进程间通信 - 图9,信号量为进程间通信 - 图10,共享存储器为进程间通信 - 图11。该标识符是单调递增的,直到超出一个整数(取决于系统实现)的最大值,然后又从零开始。

标识符是内部名,而每个IPC的外部名称之为进程间通信 - 图12。无论何时创建进程间通信 - 图13都应该指定一个进程间通信 - 图14,然后根据进程间通信 - 图15得到内部标识符。

4.消息队列

消息队列是保存在内核中的消息列表,将数据分成独立的数据单元,并将这些数据单元放到消息队列中供读取。因此,不适合较大的数据传输。
由于消息队列存在于内核中,因此写数据的时候需要从用户态转换到内核态,而读数据的时候要从内核态转化成用户态,存在切换的开销。

消息队列原来的目的是提供高于其他进程间通信 - 图16的传输效率,但现在与其他进程间通信 - 图17的速度没有太多区别,同时因为不适合大数据传输等原因,现在不应该使用消息队列了。

5.信号量

信号量的本质就是计数器,实现进程间的互斥和同步,而不是用于数据的互通。它常作为一种锁机制。防止某进程在访问资源时其它进程也访问该资源。因此。主要作为进程间以及同一个进程内不同线程之间的同步手段。其相关API如下:

  1. int semget(key_t key,int nsems,int flag);
  2. int semctl(int semid,int semnum,int cmd,...)
  3. /*
  4. 第四个参数是可选的,取决于cmd。如果使用该参数,其类型是semun,是一个union
  5. */
  6. union semun{
  7. int val;
  8. struct semid_ds *buf;
  9. unsigned short *array;
  10. }
  11. int semop(int semid,struct sembuf semoparray[],size_t nops);

信号量表示资源的数量,控制信号量的方式有两种原子操作:

  • 一个是 P 操作,这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
  • 另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;

最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。

6.共享内存

即对两个进程的虚拟内存映射到同一块物理内存中,实现共享内存。相关API如下:

  1. #include <sys/ipc.h>
  2. #include <sys/shm.h>
  3. int shmget(key_t key, size_t size, int shmflg);
  4. /*
  5. - 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。
  6. 新创建的内存段中的数据都会被初始化为0
  7. - 参数:
  8. - key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。
  9. 一般使用16进制表示,非0值
  10. - size: 共享内存的大小
  11. - shmflg: 属性
  12. - 访问权限
  13. - 附加属性:创建/判断共享内存是不是存在
  14. - 创建:IPC_CREAT
  15. - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
  16. IPC_CREAT | IPC_EXCL | 0664
  17. - 返回值:
  18. 失败:-1 并设置错误号
  19. 成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。
  20. */
  21. void *shmat(int shmid, const void *shmaddr, int shmflg);
  22. int shmdt(const void *shmaddr);
  23. /*
  24. - 功能:解除当前进程和共享内存的关联
  25. - 参数:
  26. shmaddr:共享内存的首地址
  27. - 返回值:成功 0, 失败 -1
  28. */
  29. int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  30. /*
  31. - 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。
  32. - 参数:
  33. - shmid: 共享内存的ID
  34. - cmd : 要做的操作
  35. - IPC_STAT : 获取共享内存的当前的状态
  36. - IPC_SET : 设置共享内存的状态
  37. - IPC_RMID: 标记共享内存被销毁
  38. - buf:需要设置或者获取的共享内存的属性信息
  39. - IPC_STAT : buf存储数据
  40. - IPC_SET : buf中需要初始化数据,设置到内核中
  41. - IPC_RMID : 没有用,NULL
  42. */
  43. key_t ftok(const char *pathname, int proj_id);
  44. /*
  45. - 功能:根据指定的路径名,和int值,生成一个共享内存的key
  46. - 参数:
  47. - pathname:指定一个存在的路径
  48. /home/nowcoder/Linux/a.txt
  49. /
  50. - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
  51. 范围 : 0-255 一般指定一个字符 'a'
  52. */

共享内存使用步骤:

  • 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
  • 使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
  • 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
  • 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
  • 调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。
  1. // write process
  2. #include <stdio.h>
  3. #include <sys/ipc.h>
  4. #include <sys/shm.h>
  5. #include <string.h>
  6. int main(){
  7. //1.创建共享内存
  8. int shmid = shmget(100,4096,IPC_CREAT|0664);
  9. printf("shmid : %d\n",shmid);
  10. //2.和当前进程相关联
  11. void *ptr = shmat(shmid,NULL,0);
  12. //3.写数据
  13. char *str = "hello world";
  14. memcpy(ptr,str,strlen(str)+1);
  15. getchar();//阻塞进程,以便运行另一个进程,否则开辟完的这块内存又会被关闭
  16. //4.解除关联
  17. shmdt(ptr);
  18. //5.删除共享内存
  19. shmctl(shmid,IPC_RMID,NULL);
  20. return 0;
  21. }
  1. // read process
  2. #include <stdio.h>
  3. #include <sys/ipc.h>
  4. #include <sys/shm.h>
  5. #include <string.h>
  6. int main(){
  7. //获取一个共享内存
  8. int shmid = shmget(100,0,IPC_CREAT);
  9. printf("shmid : %d\n",shmid);
  10. //2.然后关联
  11. void *ptr = shmat(shmid,NULL,0);
  12. //3.读取数据
  13. printf("recv: %s\n",(char*)ptr);
  14. getchar();
  15. //4.解除关联
  16. shmdt(ptr);
  17. //5.删除共享内存
  18. shmctl(shmid,IPC_RMID,NULL);
  19. return 0;
  20. }

如何知道共享内存的相关信息?内核为每个共享存段维护了以下数据结构:

  1. struct shmid_ds{
  2. struct ipc_perm shm_perm; /*操作权限*/
  3. int shm_segsz; /*段的大小(以字节为单位)*/
  4. time_t shm_atime; /*最后一个进程附加到该段的时间*/
  5. time_t shm_dtime; /*最后一个进程离开该段的时间*/
  6. time_t shm_ctime; /*最后一个进程修改该段的时间*/
  7. unsigned short shm_cpid; /*创建该段进程的pid*/
  8. unsigned short shm_lpid; /*在该段上操作的最后1个进程的pid*/
  9. short shm_nattch; /*当前附加到该段的进程的个数*/
  10. /*下面是私有的*/
  11. unsigned short shm_npages; /*段的大小(以页为单位)*/
  12. unsigned long *shm_pages; /*指向frames->SHMMAX的指针数组*/
  13. struct vm_area_struct *attaches; /*对共享段的描述*/
  14. };

可以通过shmctl函数获取。

7.信号

以上的通信基于的都是常规状态的工作模式,而异常状况下要利用信号来通知进程。

信号是进程间通信机制唯一的异步通信机制。

捕捉信号的函数为

  1. void (*signal(int signo,void (*func)(int)))(int);

其中signo是希望捕捉的信号,func是捕捉到该信号之后执行的函数。

进程可以通过修改sigpromask来忽略信号,但是SIGKILL和SEGSTOP是不能忽略和捕捉的。

8.套接字SOCKET

  1. int socket(int domain, int type, int protocal)

可以用于跨网络,不同主机之间的进程通信。

在网络编程一栏中详细介绍,这里不再赘述。


扩展与参考

mmap详细分析