【进程与线程】进程之间的通信
这篇文章详细解释进程间通信(IPC)的各种方式,包括早期的方法、System V的机制以及现代的套接字。早期的是无名管道、有名管道和信号。System V的包括消息队列、共享内存和信号量。现在主要使用套接字,特别是网络编程中的应用,套接字实现了不同主机之间的通信,是现在最常用的主流通信方式。
大纲:
- 无名管道(匿名管道):
-  用于有亲缘关系的进程间通信,比如父子进程。 
-  半双工,单向通信。 
-  使用pipe()系统调用创建。 
-  需要记得关闭不需要的文件描述符。 
-  示例代码可能包括创建管道,fork进程,然后父子进程分别读写。 
- 有名管道(FIFO):
-  可以在无关进程之间使用,因为通过文件系统中的命名管道文件进行通信。 
-  使用mkfifo()创建,然后像普通文件一样打开、读写。 
-  需要处理阻塞问题,比如同时打开读写端。 
- 信号(Signal):
-  用于通知进程发生了某个事件,比如中断或子进程终止。 
-  使用kill()发送信号,signal()或sigaction()注册处理函数。 
-  需要注意信号处理函数的可重入性和异步安全。 
- System V IPC:
-  消息队列:允许进程发送格式化的数据块,每个消息有类型,可以非先进先出顺序读取。 
-  共享内存:多个进程可以访问同一块内存区域,需要配合信号量进行同步。 
-  信号量:用于进程间同步,控制对共享资源的访问。 
- 套接字(Socket):
-  支持不同主机间的通信,可以是TCP或UDP。 
-  本地套接字也可以用于同一台机器上的进程通信。 
-  涉及bind(), listen(), accept(), connect(), send(), recv()等函数。 
接下来,对于每个部分的细节进行详细解析,比如API函数的参数、使用步骤,以及示例代码的正确性。同时,要注意对比不同IPC机制的特点,比如速度、复杂度、适用场景等。
我提供的代码在我的Ubuntu上运行是可行的,您可以直接复制粘贴使用即可。为了确保示例代码的正确性和简洁性,特别是信号处理和共享内存同步部分,这些地方容易出错。你在使用我的代码的时候,还是需要检查每个系统调用的返回值,处理错误情况,比如管道或FIFO创建失败时的处理。在基于对编程流程的理解之上,进行代码复用(不要无脑抄,谢谢)。
我会确保文章的结构清晰,每个IPC方法分点说明,特点、API、示例分开展示,便于读者理解。
进程间通信(IPC)的详细分类和说明,涵盖早期、System V 和现代方法:
早期:
    无名管道
    有名管道
    信号通信
System V:
    消息队列
    共享内存
    信号量
现在:
    套接字  --- 网络编程 -- 实现了不同主机之间的通信
    
    无名管道: --  管道在内核空间
    	注意事项:
            1 - 无名管道只能作用域亲缘关系之间的进程通信
            2 - 无名管道有固定的读端以及写端
            3 - 如果管道中无数据,则读操作会阻塞
            4 - 无名管道不能用于存储数据
            5 - 如果没有读操作则写操作没有意义
            6 - 如果管道满了则写操作会阻塞 
            7 - 是一个半双工通信
                单工:数据只能单向传递  A ---> B
                双工:数据可以双向发送  A <---> B
                半双工通信:数据在同一时刻只能单向发送
                全双工通信:数据可以在任意时刻双向发送
    有名管道:
    	特点:
			1- 有名字 -- 由文件的实体
            2- 有名管道可以作用于非亲缘进程间的通信
            3- 可以使用文件IO的相关函数进行操作
            4- 管道只是数据的传递,本身不存储任何数据,管道文件大小恒定为0
            5- 管道文件不能光标的偏移和定位(fseek/ftell禁用)
            6- 如果管道中没有数据则读操作会阻塞
            7- 每次读数据能够读取到 size个大小
            8- 全双工通信
	信号通信 
        特点:
            1- 唯一的异步通信
            2- 信号属于内核的内容
            3- 信号不直接传输数据,只是给一个提醒
            4- 不能自定义信号
            5- 进程对信号的响应方式
                5.1> 默认处理(缺省操作) 9) SIGKILL  19) SIGSTOP  只能默认处理 ***
                5.2> 自定义处理(信号捕捉) 10) SIGUSR1 12) SIGUSR2 只能被捕捉 *****
                5.3> 忽略处理 SIGKILL及SIGSTOP 不能被忽略 ****
            6- 软件层次上对中断机制的模拟
   ------------------------------------------------------------------------------------         
    IPC通信对象:  Inter  Process Communication
        System V:
            共享内存
            信号量
            消息队列
		
	1-共享内存
        1> 共享内存是内核实际存在的一块物理内存 
        2> 共享内存是通信效率最高的通信方式
        3> 共享内存的读操作主动不会阻塞(共享内存的数据不会主动消失,bzero清空后,读操作也会阻塞); 
    -------------------------------------
    key        shmid      owner      perms      bytes      nattch          status 
    键值        id号       拥有者    权限       大小       当前使用的进程数       状态
	
	操作:
        1> 获得键值
            - 直接指定 -- 容易重复
            - 系统自带的宏 - IPC_PRIVAT - 只会创建不会打开
            - ftok() 自动获取 - 推荐
        2> 打开/创建 共享内存 
           共享内存的ID号 =  shmget(key,大小,权限);
        
        3> 映射共享内存
            映射后的地址 =  shmat(ID号,NULL,权限);
                0 : 可读可写
                SHM_RDONLY:只读
                
                映射成功后就可以进行读写操作了
        
        4> 取消映射
            shmdt(映射后的地址);
        
        5> 删除共享内存 -- 可选
            shmctl() -- 操作共享内存
            shmctl(ID号,指令选择,存放信息的结构体)
                cmd:
                    IPC_STAT : 查看属性
                    IPC_SET  : 设置属性
                    IPC_RMID : 删除共享内存
   ------------------------------------------------------------------------------------        	   
    2-信号量 - 信号灯
    注意: 和信号没有任何关系
        1> 信号灯是内核VAL资源
        2> PV操作 P操作:申请资源 V:释放资源
        3> IPC创建的是信号灯集
        4> 信号灯不进行任何数据的传输,作为实现进程同步的工具
        5> 信号灯的值不会主动归0
    key        semid      owner      perms      nsems 
                                                信号灯集中信号灯的个数
	
	操作:
		1-获得键值 - 同共享内存
        2-打开/创建 信号灯集
            信号灯集ID =  semget(key,信号灯的个数,权限 -shmget一样)
        
        3- 执行PV操作
            semop(ID号, 指令选择,要操作信号灯的个数)
            
                指令选择:
                struct sembuf{
                    unsigned short sem_num;  /* semaphore number 信号灯的编号*/
                    short          sem_op;   /* semaphore operation PV操作的选择  */
                                                // P: <0   V: >0
                    short          sem_flg;  /* operation flags 权限选择 */
                                                0, 会阻塞进程 IPC_NOWAIT, 不会阻塞进程 
                } 
        4- 操作信号灯集
            semctl(ID号,信号灯的编号,指令选择,/* 相关信息的联合体 */)
                指令选择:
                    SETVAL:设置信号灯的值  -- 此时需要第四个参数
                    GETVAL:用于查看信号灯的值
                    IPC_RMID:删除信号灯
                    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) */
                    };  
	1-消息队列 
        1> 先进先出的队列
        2> 消息队列的格式:  消息类型 + 消息正文
	 key        msqid      owner      perms      used-bytes              messages  
                                            已经使用的字节数        当前消息队列中未读的消息数
   ------------------------------------------------------------------------------------   
	Uinx 套接字
		. Unix 套接字可用于本地进程间通信。
		. 创建套接字时使用本地协议 PF_LOCAL / PF_UNIX / AF_LOCAL / AF_UINX。
		. 可以创建流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),面向连接的套接字
 (SOCK_SEQPACKET)。数据报套接字是可靠的,既不会丢失消息,也不会传递出错。
		. Unix 套接字是套接字和管道之间的混合物。
		. 和其他进程间通信方式比,Unix 本地套接字使用方便,效率也高。因为它不需要经过网络协议栈、
		  不需要打包拆包、不需要计算校验和、不需要维护序号和应答等,只是将应用层数据从一个进程复制到另一个进程。
		. 常用于前后台进程通信,比如 X Window。
		. 另外,Unix 本地套接字可用于传递文件描述符、传递用户凭证等场景。
		
		Unix 套接字地址类型:
			struct sockaddr_un {
			    sa_family_t sun_family;               /* AF_UNIX */
			    char        sun_path[108];            /* pathname */
			};
		
	   Unix 套接字在 struct sockaddr_un 结构体的 sun_path 数组中有三种不同的地址类型:
		
	   . pathname 文件系统文件名。服务器和客户端可各自指定文件系统中的文件名,这个文件名的类型是
	     套接字类型(在 ls -l 的模式中含 s,如 srw-rw-r--),文件名必须是以空字符结束的字符串。
	     文件名保存在 sun_path 数组中,因此长度不能超 107。此地址必须调用 bind() 进行绑定。该文件
	     用仅于向客户进程告知套接字名称,既不能打开,也不能由应用程序用于通信。如果在 bind() 时该
	     文件已存在则 bind() 失败,关闭套接字时它也不会自动删除。因此一般在应用程序结束前删除它。
	     为可移植性考虑,绑定时的地址长度可以使用以下方法计算:
	   
	     offsetof(struct sockaddr_un, sun_path) + strlen(sun_path)
	   
	     offsetof() 是在 <stddef.h> 中定义的宏,它计算 sun_path 成员从结构开始处的偏移量。
	
	   . 无名。未用 bind() 绑定的流套接字是无名的,同样地用 socketpair() 创建的两个套接字也是无名
	     的。显然如果使用 bind() 的话则只有客户端可以使用无名套接字,而服务器不能使用无名套接字,
	     因为那样客户端发送给谁?用 socketpair() 创建的两个套接字因为是无名的,因此只能用于有亲缘
	     关系的进程。
	   
	   . 抽象文件名。使用抽象文件名的好处是不用在文件系统中创建文件,也不删除文件。在 sun_path[0] 
	     中指定空字符 '\0',即可使用抽象文件名的套接字。又分两种情况,一是将抽象文件名保存在
	     sun_path[1] 开始处,注意名称长度不能超过 106。绑定时地址长度可按以下计算:
	   
	     offsetof(struct sockaddr_un, sun_path) + strlen(sun_path + 1) + 1
	   
	     这种情况服务器和客户端都可以使用抽象文件名。
	
	     二是自动生成抽象文件名。即清空 sun_path 数组,而绑定的地址长度为:
	   
	     offsetof(struct sockaddr_un, sun_path)
	
	     自动生成的抽象文件名长度为 5 字节,字符范围是 [0-9a-f]。显然自动生成抽象文件名,也只能用
	     于客户端。
一、早期 IPC 方法
1. 无名管道(匿名管道,Pipe)
详情请参见文章:【进程与线程】无名管道(匿名管道,Pipe)
- 特点: 
  - 仅适用于 父子进程或有亲缘关系的进程。
- 半双工(单向通信),数据只能单向流动。
- 基于文件描述符,通过内存缓冲区传输数据。
 
- API: 
  - int pipe(int fd[2]):创建管道,- fd[0]为读端,- fd[1]为写端。
 
- 示例:
#include <stdio.h>
#include <unistd.h>
int main() {
    int fd[2];
    char buf[20];
    pipe(fd);  // 创建管道
    if (fork() == 0) {  // 子进程
        close(fd[0]);   // 关闭读端
        write(fd[1], "Hello", 6);
        close(fd[1]);
    } else {            // 父进程
        close(fd[1]);   // 关闭写端
        read(fd[0], buf, sizeof(buf));
        printf("Received: %s\n", buf);
        close(fd[0]);
    }
    return 0;
}
2. 有名管道(FIFO)
详情请参见文章:【进程与线程】有名管道(FIFO)
- 特点: 
  - 通过 文件系统中的命名管道文件 实现,无关进程可通信。
- 支持 阻塞和非阻塞模式。
 
- API: 
  - int mkfifo(const char *path, mode_t mode):创建命名管道文件。
 
示例:
// 进程 A(写端)
#include <stdio.h>
#include <fcntl.h>
int main() {
    mkfifo("/tmp/myfifo", 0666);
    int fd = open("/tmp/myfifo", O_WRONLY);
    write(fd, "Hello FIFO", 11);
    close(fd);
    return 0;
}
// 进程 B(读端)
#include <stdio.h>
#include <fcntl.h>
int main() {
    int fd = open("/tmp/myfifo", O_RDONLY);
    char buf[20];
    read(fd, buf, sizeof(buf));
    printf("Received: %s\n", buf);
    close(fd);
    return 0;
}
3. 信号(Signal)
详情请参见文章:【进程与线程】信号(Signal)
- 特点: 
  - 用于 异步通知进程 某个事件的发生(如 SIGINT、SIGTERM)。
- 信号处理函数需简短且可重入。
 
- 用于 异步通知进程 某个事件的发生(如 
- API: 
  - void (*signal(int sig, void (*handler)(int)))(int):注册信号处理函数。
- int kill(pid_t pid, int sig):向指定进程发送信号。
 
示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig) {
    printf("Received signal: %d\n", sig);
}
int main() {
    signal(SIGINT, handler);  // 捕获 Ctrl+C
    while (1) {
        sleep(1);
    }
    return 0;
}
二、System V IPC
1. 消息队列(Message Queue)
- 特点: 
  - 消息按类型存储,支持 优先级读取。
- 独立于进程存在(需显式删除)。
 
- API: 
  - int msgget(key_t key, int msgflg):创建或获取消息队列。
- int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg):发送消息。
- ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg):接收消息。
 
示例:
// 发送端
#include <sys/msg.h>
struct msgbuf {
    long mtype;
    char mtext[100];
};
int main() {
    int msqid = msgget(1234, IPC_CREAT | 0666);
    struct msgbuf msg = {1, "Hello Message Queue"};
    msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
    return 0;
}
// 接收端
int main() {
    int msqid = msgget(1234, 0666);
    struct msgbuf msg;
    msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0);
    printf("Received: %s\n", msg.mtext);
    msgctl(msqid, IPC_RMID, NULL);  // 删除队列
    return 0;
}
2. 共享内存(Shared Memory)
- 特点: 
  - 速度最快 的 IPC 方式,进程直接读写同一内存区域。
- 需配合信号量同步。
 
- API: 
  - int shmget(key_t key, size_t size, int shmflg):创建或获取共享内存。
- void *shmat(int shmid, const void *shmaddr, int shmflg):附加共享内存。
- int shmdt(const void *shmaddr):分离共享内存。
 
示例:
// 写入端
#include <sys/shm.h>
int main() {
    int shmid = shmget(1234, 1024, IPC_CREAT | 0666);
    char *shm = shmat(shmid, NULL, 0);
    sprintf(shm, "Hello Shared Memory");
    shmdt(shm);
    return 0;
}
// 读取端
int main() {
    int shmid = shmget(1234, 1024, 0666);
    char *shm = shmat(shmid, NULL, 0);
    printf("Received: %s\n", shm);
    shmdt(shm);
    shmctl(shmid, IPC_RMID, NULL);  // 删除共享内存
    return 0;
}
3. 信号量(Semaphore)
- 特点: 
  - 用于 进程间同步,控制对共享资源的访问。
- 支持原子操作(P/V 操作)。
 
- API: 
  - int semget(key_t key, int nsems, int semflg):创建或获取信号量集。
- int semop(int semid, struct sembuf *sops, size_t nsops):执行信号量操作。
 
示例:
#include <sys/sem.h>
int main() {
    int semid = semget(1234, 1, IPC_CREAT | 0666);
    semctl(semid, 0, SETVAL, 1);  // 初始化信号量为1
    struct sembuf op = {0, -1, 0};  // P操作(获取资源)
    semop(semid, &op, 1);
    printf("Critical section start\n");
    sleep(2);
    printf("Critical section end\n");
    op.sem_op = 1;  // V操作(释放资源)
    semop(semid, &op, 1);
    semctl(semid, 0, IPC_RMID);  // 删除信号量
    return 0;
}
三、现代 IPC:套接字(Socket)
- 特点: 
  - 支持 跨网络通信(TCP/UDP)。
- 本地套接字(Unix Domain Socket)也可用于同一主机的进程通信。
- 全双工通信。
 
- API: 
  - socket():创建套接字。
- bind()、- listen()、- accept()(服务端)。
- connect()、- send()、- recv()(客户端)。
 
示例(本地 TCP 通信):
// 服务端
#include <sys/socket.h>
#include <sys/un.h>
int main() {
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    struct sockaddr_un addr = {AF_UNIX, "/tmp/mysocket"};
    bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(sockfd, 5);
    int client = accept(sockfd, NULL, NULL);
    send(client, "Hello Socket", 12, 0);
    close(client);
    close(sockfd);
    unlink("/tmp/mysocket");
    return 0;
}
// 客户端
int main() {
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    struct sockaddr_un addr = {AF_UNIX, "/tmp/mysocket"};
    connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    char buf[20];
    recv(sockfd, buf, sizeof(buf), 0);
    printf("Received: %s\n", buf);
    close(sockfd);
    return 0;
}
对比与适用场景
根据适合场景的需求选择合适的 IPC 机制:
- 简单通信:无名管道、信号。
- 高性能共享:共享内存 + 信号量。
- 跨网络通信:套接字。
| IPC 方法 | 速度 | 复杂度 | 适用范围 | 同步需求 | 
|---|---|---|---|---|
| 无名管道 | 快 | 低 | 父子进程 | 无需显式同步 | 
| 有名管道(FIFO) | 中 | 中 | 任意进程 | 无需显式同步 | 
| 信号 | 快 | 低 | 异步通知 | 信号处理函数 | 
| 消息队列 | 中 | 高 | 任意进程 | 内置消息类型管理 | 
| 共享内存 | 最快 | 高 | 高性能数据共享 | 需信号量同步 | 
| 信号量 | 快 | 高 | 进程同步 | 原子操作 | 
| 套接字 | 中 | 高 | 跨网络或本地进程 | 协议层控制 | 
以上。仅供学习与分享交流,请勿用于商业用途!转载需提前说明。
我是一个十分热爱技术的程序员,希望这篇文章能够对您有帮助,也希望认识更多热爱程序开发的小伙伴。
 感谢!
