多进程和多线程间通信总结[一]
- 1. 进程间通信
- 1.1 socket - 网络中进程间通信
- 1.1.1 socket
- 1.1.2 代码示例
- 1.2 消息队列 - 同一主机上的进程通信方式
- 1.2.1. Linux中的消息队列其实就是一个文件。
- 1.2.2. 消息队列的本质其实是一个内核提供的链表。
- 1.2.3. 消息队列的缺点
- 1.2.4 消息队列接口:ftok msgget msgsnd msgrcv msgctl 接口
- 1.2.1 伪代码示例:
- 1.3 信号量(Semaphore) - 同一主机上的进程通信方式
- 1.3.1 信号量接口 - semget semctl semop
- 1.4 信号(Signal) - 同一主机上的进程通信方式
- 1.4.1 常用信号
- 1.4.2 信号处理的几个函数: Signal Sigaction kill 和 alarm
- 1.5 管道(PIPE) - 同一主机上的进程通信方式
- 1.5.1 匿名管道
- 1.5.2 命名管道
- 1.5.3 总结
- 2. 线程间通信
- 2.1 互斥量+条件变量
- 2.1.1 伪代码示例:
- 2.1.2 实际代码示例
- 2.1.3 条件变量始终与互斥锁一起使用。
- 2.2 eventfd - 通知/等待机制
- 2.2.1 简单/伪代码实现:
- 2.2.2 其他eventfd示例及其参考我的博客
- 2.3 信号量
- 2.3.1 简单/伪代码实现:
- 2.3.2 详细及示例代码参考我的博客
- 2.4 信号
- 2.5 锁机制:包括互斥锁、条件变量、读写锁和自旋锁。
- 2.5.1 互斥锁
- 2.5.2 读写锁
- 2.5.3 条件变量
- 2.5.4 自旋锁
1. 进程间通信
进程间通信分为两个:
- 同一个主机上的进程间通信:消息队列,信号量(Semaphore), 共享内存(Shared Memory),管道(PIPE), 有名管道(FIFO), 和信号(Signal)
- 网络中的进程间通信;Socket
1.1 socket - 网络中进程间通信
1.1.1 socket
说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口
主要功能是将进程发送的各种请求,映射到创建套接口时指定的,与协议有关的具体实现上
可以理解为: socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)
图示例:
图示例参考:
1.1.2 代码示例
/* 客户端 创建,链接,读写数据 */
socket(AF_INET, SOCK_STREAM, 0)
connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)
write(sockfd, cCliSendMsg, sizeof(cCliSendMsg));
read(sockfd, cSerRcvMsg, sizeof(cSerRcvMsg))
/* 服务端 创建,绑定端口,监听,接收,读写数据 */
socket(AF_INET, SOCK_STREAM, 0)
bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)
listen(sockfd, 5);
accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
read(newsockfd, cCliRcvMsg, sizeof(cCliRcvMsg));
write(newsockfd, cSerSendMsg, sizeof(cSerSendMsg));
参考:
之前的文章:socket(6):代码示例
1.2 消息队列 - 同一主机上的进程通信方式
1.2.1. Linux中的消息队列其实就是一个文件。
1.2.2. 消息队列的本质其实是一个内核提供的链表。
内核基于这个链表,实现了一个数据结构,并且通过维护这个数据结构来维护这个消息队列。
向消息队列中写数据,实际上是向这个数据结构中插入一个新结点;
从消息队列汇总读数据,实际上是从这个数据结构中删除一个结点。
1.2.3. 消息队列的缺点
- 每个消息的最大长度是有上限的(MSGMAX) 其中,ubuntu下 MSGMAX = 8192
- 每个消息队列的总的字节数(MSGMNB) 其中,ubuntu下 MSGMNB= 16384
- 系统上消息队列的总数上限(MSGMNI) 其中,ubuntu下 MSGMNI= 32000
- 可以用cat /proc/sys/kernel/msgmax查看具体的数据
1.2.4 消息队列接口:ftok msgget msgsnd msgrcv msgctl 接口
/* 创建key_t值,称为IPC键值 */
key_t ftok(const char *pathname, int proj_id)
/* 创建和访问一个消息队列 */
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);
/* 消息队列的控制函数,如删除 */
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
1.2.1 伪代码示例:
/* 创建消息队列对象 iMsgId */
INT iMsgId;
key_t key;
/* pcPath 具体文件,函数ftok把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为IPC键值*/
key = ftok(pcPath, 0x12345678);
iMsgId = msgget(key, IPC_CREAT|0700);
/* 发送消息 */
typedef struct tagMSGBUF
{
long lMtype;
char acMtext[1]; /* 可变数组 */
}MSG_BUF_S; /* MSG_BUF_S 消息类型是 msgrcv 规定的格式 */
MSG_BUF_S *pstMsg = NULL;
CHAR *pcBuffer = NULL;
pstMsg->lMtype = 1; /* 没用 */
memcpy(pstMsg->acMtext, pcBuf, sizeof(pstMsg->acMtext)); /* pcBuf 具体消息 */
key = ftok(pcPath, 0x12345678);
iMsgId = msgget(key, 0700); /* 取消息队列id */
iRet = msgsnd(iMsgId, pstMsg, (size_t)(UINT)iLen, (INT)0); /* iLen 是 pcBuf 的长度 */
/* 接收消息 */
/* MSG_BUF_S 消息类型是msgrcv 规定的格式*/
MSG_BUF_S *pstMsg = NULL;
pstMsg = (MSG_BUF_S*)pcBuffer; /* pcBuffer malloc 的缓冲区 */
pstMsg->lMtype = 1;
iLength = msgrcv(iFd, pstMsg, (size_t)(UINT)iBuflen, (LONG)1, IPC_NOWAIT|MSG_NOERROR);
/* 将 msg 拷贝至 buf */
memcpy(pcBuf, pstMsg->acMtext, (size_t)(UINT)iLength);
/* 然后读取消息 */
参考:
我的博客:Linux-进程间通信(1)消息队列 msg
1.3 信号量(Semaphore) - 同一主机上的进程通信方式
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。
它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。
因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
1.3.1 信号量接口 - semget semctl semop
信号量的创建
int semget(key_t key, int nsems, int semflg);
信号量的初始化/删除
int semctl(int semid, int semnum, int cmd, ...);
信号量操作, P操作,V操作,都是通过一个函数实现的
int semop(int semid, struct sembuf *sops, unsigned nsops);
信号量的删除
semctl(int sem_id)
1.4 信号(Signal) - 同一主机上的进程通信方式
信号是操作系统响应某些条件而产生的一个事件
1.4.1 常用信号
SIGALRM 定时器信号
SIGINT 终端终端ctrl+c
SIGKILL 终止信号
SIGHUP 链接中断
SIGABORT 进程异常终止
SIGQUIT 终端退出
SIGTERM 终止
SIGSEGV 无效内存段访问
SIGCHLD 子进程停止或退出
SIGSTOP 停止执行
SIGSTP 终端挂起
1.4.2 信号处理的几个函数: Signal Sigaction kill 和 alarm
Signal功能:处理指定的信号,主要是处理忽略(SIG_IGN)和恢复(SIG_DFL)
igaction功能:处理指定的信号,比Signal函数更加健壮的信号处理接口
kill功能:进程可以通过kill函数向包括它本身在内的其他进程发送一个信号
alarm功能:提供了一个闹钟的功能;进程可以通过调用alarm函数在经过预定的时间后向进程发送一个SIGALRM信号。
参考:
我的博客:通俗易懂说信号
1.5 管道(PIPE) - 同一主机上的进程通信方式
管道本质上是内核的一块缓存
1.5.1 匿名管道
匿名管道是基于文件描述符的通信方式。实现两个进程间的通信时必须通过fork创建子进程,实现父子进程之间的通信
- 特点
只能够进行单向通信
只能够用于有血缘关系(父子,兄弟,爷孙)的进程之间,多常用于父子之间
int mkfifo(const char *filename,mode_t mode);
【参数】:
filename:创建的有名管道的全路径名
mode:创建的命名管道的模式,指明其存取权限
1.5.2 命名管道
命名管道本质上是一个管道文件,可以通过命令创建也可以通过函数创建,用户可以看到
- 特点
可以进行不相干进程间的通信
命名管道是一个文件,对于文件的相关操作对其同样适用
1.5.3 总结
类型 进程关系 不同点 本质
匿名管道 必须是亲缘关系 由pipe创建并打开 内核的一块缓存
命名管道 两个毫不相干进程 由mkfifo创建,open打开 一个文件
管道参考:
https://www.linuxprobe.com/linux-process-method.html
2. 线程间通信
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
2.1 互斥量+条件变量
互斥量用来确保一个线程独占一个资源的访问
互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定,所以经常导致死锁。
一个/多个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"信号。
条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用。
2.1.1 伪代码示例:
In Thread1:消费者
pthread_mutex_lock(&mutex); // 拿到互斥锁,进入临界区
while( 条件为假)
pthread_cond_wait(cond, mutex); // 令进程等待在条件变量上
//修改条件
pthread_mutex_unlock(&mutex); // 释放互斥锁
In Thread2:生产者
pthread_mutex_lock(&mutex); // 拿到互斥锁,进入临界区
//设置条件为真
pthread_cond_signal(cond); // 通知等待在条件变量上的消费者
pthread_mutex_unlock(&mutex); // 释放互斥锁
2.1.2 实际代码示例
定义
typedef struct tagTestThread
{
/* other */
pthread_cond_t stExternalCond;
pthread_mutex_t stExternalLock;
}MSG_THREAD_S;
TEST_THREAD_S *pstThread;
/* 条件变量等待时间 */
struct timespec stTimeOut;
stTimeOut.tv_sec = 1L;
stTimeOut.tv_nsec = 0;
写消息
(VOID)pthread_mutex_lock(&(pstThread->stExternalLock));
(VOID)pthread_cond_signal(&(pstThread->stExternalCond));
(VOID)pthread_mutex_unlock(&(pstThread->stExternalLock));
等待消息
(VOID)pthread_mutex_lock(&(pstThread->stExternalLock));
(VOID)pthread_cond_timedwait(&(pstThread->stExternalCond),
&(pstThread->stExternalLock),
&stTimeOut);
(VOID)pthread_mutex_unlock(&(pstThread->stExternalLock));
互斥量+条件变量及其参考我的博客:
多线程(8)多线程同步之互斥量+条件变量(linux实现)
2.1.3 条件变量始终与互斥锁一起使用。
2.2 eventfd - 通知/等待机制
主要用于进程或者线程间的通信(如通知/等待机制的实现)
2.2.1 简单/伪代码实现:
定义
ievFD = eventfd(0, EFD_SEMAPHORE);
EFD_SEMAPHORE 提供类似信号量语义的 read 操作,简单说就是计数值 count 递减 1。
写
eventfd_t iValue = 1;
/* 消息就绪 */
int iWriteRet = eventfd_write(iEventFd, iValue);
读
int iReadRet = eventfd_read(iEventFd, &iValue);
/* 读消息 */
2.2.2 其他eventfd示例及其参考我的博客
通俗易懂说多路复用(3)eventfd 事件通知
2.3 信号量
信号量(Semaphore),用来保证两个或多个关键代码段不被并发调用。
2.3.1 简单/伪代码实现:
/* 定义 */
sem_t sem;
sem_init(&sem,0,0);
sem_destroy(&sem);
/* 消费者线程-读取消息 */
sem_wait(&sem);
/* 读消息 */
/* 生产者线程-消息就绪 */
/* 写消息 */
sem_post(&sem);
2.3.2 详细及示例代码参考我的博客
多线程(11)多线程同步之信号量(Linux实现)
2.4 信号
Linux 用 pthread_kill 对线程发信号。
pthread_cancel 线程的取消
2.5 锁机制:包括互斥锁、条件变量、读写锁和自旋锁。
2.5.1 互斥锁
互斥锁确保同一时间只能有一个线程访问共享资源。
当锁被占用时试图对其加锁的线程都进入阻塞状态(释放CPU资源使其由运行状态进入等待状态)。
当锁释放时哪个等待线程能获得该锁取决于内核的调度。
2.5.2 读写锁
读写锁当以写模式加锁而处于写状态时任何试图加锁的线程(不论是读或写)都阻塞,
当以读状态模式加锁而处于读状态时“读”线程不阻塞,“写”线程阻塞。
读模式共享,写模式互斥。
2.5.3 条件变量
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。
对条件的测试是在互斥锁的保护下进行的。
条件变量始终与互斥锁一起使用。
2.5.4 自旋锁
自旋锁上锁受阻时线程不阻塞而是在循环中轮询查看能否获得该锁,没有线程的切换因而没有切换开销,不过对CPU的霸占会导致CPU资源的浪费。
所以自旋锁适用于并行结构(多个处理器)或者适用于锁被持有时间短而不希望在线程切换产生开销的情况。