进程间通信函数接口及应用模板小结
进程间通信方式
1.无名管道/有名管道
2.信号
3.共享队列(system V-IPC)
4.共享内存(system V-IPC)
5.信号量(system V-IPC)
6.套接字
无名管道特征
1.文件没有名字,无法使用open
2.只能用于亲缘进程间
3.半双工工作方式:读写端分开
4.写入操作不具有原子性,会被打断,因此只能用于一对一的简单场景
5.不能使用lseek()来定位
相关API
int pipe(int pipefd[2]);
应用模板
int main(int argc, char *argv[])
{
int fd[2];
if (pipe(fd) == -1)
{
fprintf(stderr, "errno:%d,%s", errno, strerror(errno));
exit(-1);
}
pid_t x = fork(); // 创建子进程,继承pipe的描述符
if (x == 0) // 子进程
{
char *s = "I am the child\n";
write(fd[1], s, strlen(s)); // 1是读,0是写
}
if (x > 0) // 父进程
{
char buf[30];
bzero(buf, sizeof(buf));
read(fd[0], buf, sizeof(buf));
printf("from the child:%s", buf);
}
close(fd[0]); // 因为无名,所以无法用open,但还是需要close
close(fd[1]);
return 0;
}
有名管道特征
1.有名字,且存储于普通文件系统中
2.任何有权限的进程都可以使用open函数获取FIFO文件描述符
4.写入操作具有原子性,支持多写者同时进行写操作且数据不会相互践踏。这是与无名管道的最大区别
5.不能使用lseek()来定位
6.FIFO,最先被写入的数据,最先被读出来
相关API
int mkfifo(const char *pathname, mode_t mode);
应用模板
#define FIFO "/tmp/fifo4test"
int main(int argc, char *argv[]) // 发送的进程
{
if (access(FIFI, F_OK)) // 检查文件是否存在,不存在就创建
{
mkfifo(FIFO, 0644);
}
int fifo_fd = open(FIFO, O_RDWR);
char *s = "I am the child\n";
int n = write(fifo_fd, s, strlen(s)); // 1是读,0是写
printf("%d bytes have been sended.\n", n);
close(fifo_fd);
return 0;
}
int main(int argc, char *argv[]) // 接收的进程
{
if (access(FIFI, F_OK)) // 检查文件是否存在,不存在就创建
{
mkfifo(FIFO, 0644);
}
int fifo_fd = open(FIFO, O_RDWR);
char buf[30];
bzero(buf, sizeof(buf));
read(fifo_fd, buf, sizeof(buf));
printf("from the child:%s", buf);
close(fifo_fd);
return 0;
}
信号特征
1.大部分信号都是异步
2.linux信号62个:
1~31是非实时,不可靠信号,响应不排队;
如果目标进程没有及时相应,则随后到达的同样的信号会被丢弃;
每个非实时信号都对应一个系统事件。
当进程的挂起信号中含有实时和非实时信号,则会优先响应实时信号并从大到小依次响应
34~64实时,可靠信号,按接收顺序排队
即使相同的实时信号被同时发送多次也不会被丢弃,而是依次响应
实时信号没有系统事件与之对应
3.对信号的处理:阻塞,被捕捉并响应(按设置的响应函数或忽略),执行默认动作
相关API
int kill(pid_t pid, int sig); // 向pid进程发送信号
void (*sighandler_t)(int); // 设置的函数,sighandler_t 被定义为指向一个接收单个 int 参数(即信号编号)并返回 void 的函数的指针。这样的函数通常被称为“信号处理函数”或“信号处理程序”。
sighandler_t signal(int signum, sighandler_t handler); // 接收signum信号,并执行handler信号处理函数,一般和kill配套使用
int raise(int sig); // 给自己发送信号
int pause(void); // 挂起等待信号
sigset_t setset; // 创建信号集
int sigemptyset(sigset_t *set); // 清空信号集
int sigfillset(sigset_t *set); // 将所有信号添加到信号集中
int sigaddset(sigset_t *set, int signum); // 将特定信号添加到信号集中
int sigdelset(sigset_t *set, int signum); // 将特定信号从信号集中删除
int sigismember(const sigset_t *set, int signum); // 判断某特定信号是否在信号集中
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); // 设置阻塞掩码
应用模板
void handler(int sig)
// sig是触发该处理函数的信号值;
// 子进程会不会继承父进程的信号响应函数和阻塞状态,每个进程都有自己的信号屏蔽字(signal mask),用于控制哪些信号在当前是阻塞的(即不会被立即处理)。
// 而且不同的信号能共享一个处理函数
{
printf("This is a test4signal.sig:%d\n", sig);
}
int main(int argc, char const *argv[])
{
sigset_t set; // 1.创建一个信号集
sigemptyset(&set); // 清空信号集
sigaddset(&set, SIGINT); // 2.把需要屏蔽的信号加入到该集合中
sigprocmask(SIG_BLOCK, &set, NULL); // 3.需要设置该集合的阻塞属性,第一个参数如果是SIG_UNBLOCK,就解除阻塞
signal(SIGUSR1, handler);
// 接收信号,第二个参数设置成SIG_IGN是忽略 SIG_DEL是删除
pause(); // 暂停进程,等待信号。此处可以用while(1),一直等待信号
return 0;
}
消息队列,共享内存,信号量 (统称为system-V IPC)
1.消息队列,提供带有数据标识的特殊管道
2.共享内存,提供一块物理内存多次映射到不同的进程虚拟空间
3.信号量,对进程或线程的资源进行管理
消息队列相关API
key_t ftok(const char *pathname, int proj_id); // 指定路径和未使用过的整数
int msgget(key_t key, int msgflg); // 获取消息队列的ID
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); // 设置消息队列的属性
应用模板
// 应用模板:
#define PROJ_PATH "."
#define PROJ_ID 10
#define A2B 1L // 定义消息标识
#define MSGSZ 100 // 定义消息长度
;
struct msgbuf // 定义带标识的消息结构体
{
long mtype;
char mtext[MSGSZ]; // 消息最大一般是16384bytes
};
int main()
{
key_t key = ftok(PROJ_PATH, PROJ_ID);
int msgid = msgget(key, IPC_CREAT | 0644);
struct msgbuf buf;
bzero(&buf, sizeof(buf));
buf.mtype = A2B;
strcpy(buf.mtext, "This is a msg test.\n");
msgsnd(msgid, &buf, strlen(buf.mtext), MSGSZ);
// 错误处理此处忽略,实际项目需要考虑
// ================下面为从消息队列读取消息============//
struct msgbuf rec;
bzero(&rec, sizeof(rec));
msgrcv(msgid, &buf, MSGSZ, A2B, 0); // 0表示等待消息,默认阻塞
printf("recive the message:%s", rec.mtext);
// ================从消息队列读取消息============//
return 0;
}
共享内存相关API
key_t ftok(const char *pathname, int proj_id); // 指定路径和未使用过的整数
int shmget(key_t key, size_t size, int shmflg); // 获取的ID
void *shmat(int shmid, const void *shmaddr, int shmflg); // 对共享内存进行映射
int shmdt(const void *shmaddr); // 解除映射
int shmctl(int msqid, int cmd, struct msqid_ds *buf); // 设置共享内存的属性
信号量相关API
key_t ftok(const char *pathname, int proj_id); // 指定路径和未使用过的整数
int semget(key_t key, int nsems, int semflg); // 获取信号量ID
int semop(int semid, struct sembuf *sops, size_t nsops); // 对信号量进行P/V操作
int semctl(int semid, int semnum, int cmd, ...); // 获取或设置信号量的相关属性
因为systemV IPC 的信号量用的较少,且进程间通信一般可以用POSIX的信号量代替,因此一起简要总结后者。
POSIX的信号量分为有名和无名:有名信号量,是一种特殊的文件,一般会放在系统的特殊文件系统/dev/shm中,不同进程间需要约定一个相同的名字,就能通过这种有名信号量来相互协调;无名信号量,一般用于一个进程内线程间的同步互斥,因为线程共享一个内存空间。
POSIX有名信号量API
sem_t *sem;
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value); // 创建/打开一个有名信号量
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 申请资源,即P操作
int sem_post(sem_t *sem); // 释放资源,即V操作
int sem_close(sem_t *sem); // 关闭
int sem_unlink(const char *name); // 删除有名信号量文件
POSIX无名信号量API
sem_t *sem;
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 申请资源,即P操作
int sem_post(sem_t *sem); // 释放资源,即V操作
int sem_destroy(sem_t *sem);
/*
有名信号量和共享内存应用模板
#define PROJ_PATH "."
#define PROJ_ID 10
#define SEMNAME "sem4test" // 定义消息标识
#define SHMSZ 100 // 定义消息长度
int main()
{
key_t key = ftok(PROJ_PATH, PROJ_ID); // 指定路径和未使用过的整数
int shm_id = shmget(key, SHMSZ, IPC_CREAT | 0644); // 获取共享内存的ID
char *shmaddr = shmat(shm_id, NULL, 0); // 对共享内存进行映射
// 创建POSIX有名信号量
sem_t *s;
s = sem_open(SEMNAME, O_CREAT, 0644, 0);
while (1)
{
fgets(shmaddr, SHMSZ, stdin);
sem_post(s); // 向信号量释放资源,即每次进行操作结束后资源量+1
}
sem_unlink(SEMNAME);
//=========================下半部分为接收信号=====================//
while (1)
{
sem_post(s); // 向信号量申请资源,当信息被读完,即信息量为0时被阻塞
printf("recive message:%s", shmaddr);
}
//=========================接收信号=====================//
sem_close(s);
return 0;
}
尤其注意,System V的信号量和POSIX的信号量不是一个概念,它们之间存在明显的区别。以下是两者之间的主要区别:
- 来源与标准:
System V信号量:来源于Unix操作系统的一个分支,即System V版本。
POSIX信号量:来源于“可移植操作系统接口(Portable Operating System Interface)”标准,这是一个由电气与电子工程学会(IEEE)开发,并由ISO(国际标准化组织)和IEC(国际电工委员会)采纳的国际标准。
- 使用场景:
System V信号量:常用于进程间的同步。
POSIX信号量:常用于线程间的同步,但也可以用于进程间同步,特别是当使用有名信号量时。
- 实现与复杂性:
System V信号量:使用相对复杂,通常涉及多个步骤和结构体(如struct sembuf)。
POSIX信号量:使用相对简单,通过单一的sem_open调用即可完成信号量的创建、初始化和权限设置。
- 存储位置:
System V信号量:基于内核,存放在内核空间中。
POSIX信号量:基于内存,信号量值通常存放在共享内存中,有名信号量通过文件系统(如/dev/shm)中的特殊文件来表示。
- 信号量类型:
System V信号量:通常作为信号量集合存在,每个集合可以包含多个信号量。
POSIX信号量:有两种类型——有名信号量和无名信号量。有名信号量通过IPC名字进行进程间同步,无名信号量则通常用于线程间同步。
- 信号量操作:
System V信号量:通过semop等系统调用来操作信号量。
POSIX信号量:通过sem_wait(P操作)、sem_post(V操作)等函数来操作信号量。
- 头文件与接口:
System V信号量:使用<sys/sem.h>头文件,接口函数包括semget、semop等。
POSIX信号量:使用<semaphore.h>头文件,接口函数包括sem_open、sem_wait、sem_post等。
剩下一项套接字,一般用于网络编程,即不同主机间的进程通信,后续补充。