每个进程都是独立的实体。一个应用程序有时候要分多个进程协同合作才能完成任务。类似一个团队开发一个项目一样,需要多人合作,合作肯定少不了沟通。一个应用程序可能也要多个进程之间也一样,进程之间有自己的交流的语言——IPC(通信机制)
无名管道
- 从名字可知,无名管道没有名字,无法open(),可以read()/write(),所以只能由父进程创建,子进程继承才能使用。
- 读写分离,有两个文件描述符,一个读端,一个写端,所以是半双工工作方式
只能用于亲缘进程之间通信(父子、兄弟)
写操作无原子性
,多进程写入就会造成数据覆盖,所以只能用于一对一通信。- 它是一种特殊文件,不存在于文件系统中,只存在于内存中;不能使用**lseek()**定位
创建无名管道:
man pipe
pipe, pipe2 - create pipe
#include <unistd.h>
int pipe(int pipefd[2]);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>
int pipe2(int pipefd[2], int flags);
//pipefd[0] —— 读端
//pipefd[1] —— 写端
RETURN VALUE
成功返回0, 错误返回-1, 以及 errno
写代码:
#include <unistd.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
int pipefd[2];
int ret = pipe(pipefd);
if (ret < 0) //创建管道失败
{
printf("faild to create pipe \r\n");
return -1;
}
pid_t pid = fork(); //创建子进程
if (pid < 0) //创建失败
{
printf("failed to create child progress \r\n");
return -1;
}
char write_buf[] = "Hello! My fahter";
if (pid == 0) //子进程
{
while (1)
{
write(pipefd[1], write_buf, strlen(write_buf)); //向管道中写入消息
sleep(1);
}
}
char read_buf[1024];
if (pid > 0)
{
while(1)
{
read(pipefd[0], read_buf, strlen(write_buf));
printf("child say : %s\r\n", read_buf);
sleep(1);
}
}
return 0;
}
有名管道
-
因为有名字,存在普通文件系统中,可以使用open来获得文件描述符。open时使用O_WRONLY只打开写端,O_RDONLY只打开读端,O_WRONLY读写端都打开。读写两端可以被多个进程同时打开。
-
普通文件一样的read()/write()读写
-
写操作具备原子性,多进程同时写入数据不会造成数据互相覆盖。
-
First In First Out
-
写进程向管道写数据,无进程读数据,写进程会堵塞。当有进程读数据,写进程恢复运行。
-
读进程读取数据,管道中没有数据,读进程阻塞。当有进程写入数据,读进程读取数据以及恢复运行。
-
**open()时带O_NONBLOCK参数打开获取fcntl()**改变fifo模式为非阻塞则不会阻塞
-
可以用于亲缘进程以及非亲缘进程之间的通信。
创建有名管道两种方式:
- shell命令 : mkfifo <管道文件名>
int mkfifo(const char *pathname, mode_t mode)
mkfifo函数man手册描述:
man 3 mkfifo
mkfifo, mkfifoat - make a FIFO special file (a named pipe)
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
//pathname —— FIFO文件名
//mode —— FIFO权限
#include <fcntl.h> /* Definition of AT_* constants */
#include <sys/stat.h>
int mkfifoat(int dirfd, const char *pathname, mode_t mode);
代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
char write_buf[] = "Linux,RT-Thread,uCos,FreeRTOS";
char read_buf[1024];
int len = strlen(write_buf);
if (access("test_fifo", F_OK)) //判断管道文件是否已经存在
{
int ret = mkfifo("test_fifo", 0644); //创建管道
if (ret < 0)
{
printf("fail to mkfifo\r\n");
return -1;
}
}
pid_t pid = fork();
if (pid < 0)
{
printf("failed to fork\r\n");
return -1;
}
if (pid == 0) //子进程
{
int fifo = open("test_fifo", O_WRONLY); //只读方式打开
write(fifo, write_buf, strlen(write_buf));
close(fifo);
}
if (pid > 0) //父进程
{
int fifo = open("test_fifo", O_RDONLY); //只写方式打开
read(fifo, read_buf, len); //从有名管道中读数据
printf("read : %s\r\n", read_buf);
close(fifo);
}
return 0;
}
执行程序在当前目录生成了有名管道文件test_fifo:
信号
一种异步通信机制。软件上模拟中断机制。
头文件
#include <signal.h> //信号相关头文件
查看信号种类
-
非实时信号
前面31个称为非实时信号,从unix系统继承下来的信号,特点:
- 不排队,会相互嵌套
- 目标进程没有及时响应,随后到达的该信号就会被丢弃、
- 每个非实时信号对应一个系统事件,事件发生,将产生对应的非实时信号
- 同时存在实时和非实时信号,实时信号的响应从大到小排队响应,非实时信号没有固定的次序。
-
实时信号
后面31个为实时信号:
- 有顺序的排队从大到小响应,不嵌套
- 同样信号发送多次,不丢次,依次响应
- 没有特殊的系统事件对应
man 7 signal //查看更详细的信号相关信息
信号产生自内核
。信号的来源主要如下:
-
用户通过输入设备让内核产生信号,如
CTRL+C
终止从终端运行的进程。 -
进程执行出错,内核产生信号。如
访问非法内存
、浮点数溢出
等。 -
进程通过系统调用
kill()
发送信号给另一个进程/raise()
进程自己给自己发信号。
信号的处理
- 默认处理,比如
CTRL+C
就会向连接到终端的运行的进程发送SIGINT
信号,默认处理下进程就会终止运行。 - 忽略信号,通过系统调用
signal()
设置忽略某个信号,不做处理。 - 通过系统调用
signal()
注册信号处理函数,当收到该信号,执行用户自定义的处理操作。 - 阻塞某些或某个信号,阻塞只是暂时挂起而不是忽略
- 特殊的两个信号:
SIGKILL
和SIGSTOP
,不能被忽略、阻塞以及捕捉
。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler); //设置信号处理函数或者忽略信号的函数
//signum —— 发送的信号
//handler —— 信号处理方式,可以是SIG_IGN、SIG_DEL或者函数
signal
函数sighandler_t handler
的取值:
SIG_IGN
//忽略 信号SIG_DFL
, //默认处理方式- sighandler_t 类型的函数 //自定义处理方式
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
void sigint_handler(int num)
{
printf("hello gcc\r\n");
}
int main(int argc, char **argv)
{
signal(SIGINT, sigint_handler); //Ctrl+C发出SIGINT信号,默认处理是结束进程,一旦设置自己的处理函数,将不再执行默认处理即不终止进程
//signal(SIGINT, SIG_DFL); //默认处理即终止进程
//sinal(SIGINT, SIG_IGN); //不做任何处理
while(1)
{
printf("hello linux\r\n");
sleep(1);
}
return 0;
}
向进程发送信号:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig); //向某指定进程发送指定信号
//pid —— 信号要发送的信号(进程号)
//sig —— 要发送的信号(查看信号表)
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
pid_t pid = fork();
if (pid == 0) //子进程
{
while(1)
{
printf("hello!!! linux\r\n");
sleep(1);
}
}
if (pid > 0) //父进程
{
sleep(10);
kill(pid, SIGSTOP); // 向子进程发送信号
}
return 0;
}
阻塞或者解除阻塞一个或多个信号:
#include <signal.h> //头文件
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//how —— 阻塞的方式
SIG_BLOCK //在原有阻塞信号基础上,增加set信号
SIG_UNBLOCK //在原有阻塞信号基础上,解除set信号
SIG_SETMASK //在原有阻塞信号基础上,替换为set信号
//set —— 新的信号
//oldset —— 原有的阻塞信号
信号发送和捕获函数plus版本
sigqueue - queue a signal and data to a process //向指向进程发送执行信号以及可以携带指定数据
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval联合体定义:
union sigval {
int sival_int;
void *sival_ptr;
};
sigaction, rt_sigaction - examine and change a signal action //捕获以及更改捕捉到信号的操作
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact); //捕捉指定信号以及可以指定捕捉后的操作,还可以通过扩展函数来获取信号携带的额数据
//signum 捕捉的信号
sigaction结构体
struct sigaction {
union {
__sighandler_t _sa_handler; //标准信号响应函数指针
void (*_sa_sigaction)(int, struct siginfo *, void *); //扩展响应信号函数指针
} _u;
sigset_t sa_mask; //信号掩码,使用信号集操作函数设置
unsigned long sa_flags;
void (*sa_restorer)(void);
};
//使用扩展信号响应函数,sa_flags必须设置SA_SIGINFO
System V IPC
包含以下三个:
消息队列
共享内存
信号量
进程每打开一个IPC对象
就会获得一个ID
,ID是可变的,之后就可以使用ID来操作IPC对象
。类似进程进程有PID,IPC对象也有类似——key
键值,key是唯一的。跟操作文件做对比,ID类似文件描述符,key类似文件路径。
ftok - convert a pathname and a project identifier to a System V IPC key (创建System V IPC对象的key)
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id); //创建key的函数
//pathname —— 路径
// proj_id —— 整数
RETURN VALUE //返回值
On success, the generated key_t value is returned. On failure -1 is returned, with errno indicating the error as for the stat(2) (成功返回key值,失败返回-1)
信号量
RTOS中的信号量和Linux中信号的概念和作用差不多。
Linux三种信号量:
system-V信号量
posix有名信号量
posix无名信号量
信号量的作用:
-
进程、线程之间的同步
-
临界资源的互斥访问。
访问临界资源前需要对资源申请,申请资源的操作就是
P操作
。退出临界区时要释放响应资源,这个操作称为
V操作
。信号量实际上就是类似用一个变量表示资源数,P/V操作
就是对该变量进行加/减一的操作。与变量加减不同的是信号量的加减具备原子性——不会被操作系统调度机制打断。
信号量的相关三个宏:
SEMMNI
—— 系统信号量总数最大值SEMMSL
—— 每个信号量值的最大值SEMMNS
—— 系统中所有信号量中信号量元素的总数最大值
semget - get a System V semaphore set identifier(创建信号)
//头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
//key —— 信号唯一关键标识,键值
//nsems —— 信号量的个数
/*
semflag —— 信号量创建的方式
IPC_CREAT —— key对应信号量不存在,创建
IPC_EXCL —— key对应信号量存在, 报错
mode —— 信号量访问权限(八进制,如0777)
*/
semop, semtimedop - System V semaphore operations(对信号进行P/V操作、等零操作)
/* 头文件 */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
/*semid —— 信号量 ID */
/*sembuf —— 信号量集合的结构指针 */
/* nsops —— sops的长度*/
struct sembuf{
short sem_num; //信号量元素序号(数组下标,从0开始)。
short sem_op; //sem_op 成员的值是信号量在一次操作中需要改变的数值。通常只会用到两个值,一个是-1 (P操作),尝试获取信号量;一个是+1 (V操作),释放信号量。
short sem_flg; //通常设为:SEM_UNDO,程序结束,信号量为 semop 调用前的值。
};
semctl - System V semaphore control operations(设置或获取信号的相关属性)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
/* smeid —— 信号量ID */
/* semnum —— 信号量序号,数组下标 */
/*
cmd —— 信号量的操作方式:
IPC_STAT —— 获取信号量的属性
IPC_SET —— 设置信号量的属性
IPC_RMID —— 立即删除该信号,semnum将被忽略
IPC_INFO —— 获得关于信号的系统限制信息
SEM_STAT —— 同IPC_STAT
GETALL —— 返回所有信号量元素的值
GETNCNT —— 返回正阻塞在对信号两元素P操作的进程总数
GETPID —— 返回最后一个对该信号量元素操作的进程的PID
GETVAL —— 返回该信号量的值
GETZCNT —— 返回正阻塞该信号量元素等零操作的进程总数
SETALL —— 设置所有信号量元素的值
*/
/*
不同cmd对应的返回值:
GETNCNT —— semncnt
GETPID —— sempid
GETVAL —— semval
GETZCNT —— semzcnt
...
*/
如若用到第四个参数,用户必须自己定义:
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) */
};
进程间的同步:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
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) */
};
int main(int argc, char **argv)
{
int semid = semget((key_t)1234, 1, 0666|IPC_CREAT);//创建信号量集
if(semid < 0)
{
printf("failed to get semid\r\n");
return -1;
}
union semun sem_num;
sem_num.val = 0;
semctl(semid, 0, SETALL, sem_num); //设置信号量值为1
struct sembuf sem_p = {
0, //信号量元素序号
-1, //信号量操作一次变化的值
0
};
struct sembuf sem_v = {
0, //信号量元素序号
1, //信号量操作一次变化的值
0 //
};
pid_t pid = fork();
if(pid == 0)
{
while(1)
{
printf("I am child\r\n");
sleep(2);
semop(semid, &sem_v, 1);
}
}
else
{
while(1)
{
semop(semid, &sem_p, 1);
printf("I am father\r\n");
}
}
return 0;
}
消息队列
消息队列类似管道,也可以说是带数据标识的特殊管道,可以读出制定数据
msgget - get a System V message queue identifier //获得一个system V的消息队列ID
//头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
//key —— 消息队列键值
/*
msgflag —— IPC_CREAT 不存在就创建
—— IPC_EXCL 存在则报错
—— mode 访问权限
*/
RETURN VALUE
成功返回值将是消息队列标识符(非负整数),失败为-1,errno指示错误。
msgrcv, msgsnd - System V message queue operations
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
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); //从消息队列中读出消息
//msgid —— 消息队列id
//msgp —— 发送or接受消息缓冲区
//msgsz —— 发送or接受数据的大小
//msgtyp —— 数据的类型
msgp参数定义如下:
/* msgp —— 消息必须组织成如下结构体 */
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
msgflg主要有下面选项:
IPC_NOWAIT //非阻塞方式读写数据
MSG_COPY (since Linux 3.8)//从msgtyp指定的序号位置获取一个消息(消息序号从0开始),一般与IPC_NOWAIT搭配使用,没有消息立即返回。MSG_COPY标志是为内核检查点恢复功能的实现添加的,是有用的。只有在内核使用CONFIG_CHECKPOINT_RESTORE选项构建时才能使用
MSG_EXCEPT //读出标识符不等msgtyp的第一个消息
MSG_NOERROR //消息大小比msgsz大,截断消息而不报错
RETURN VALUE
失败返回-1,成功 msgsnd() 返回 0, msgrcv() 返回读取的字节数
msg_recv.c:
#include <sys/types.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include "common.h"
int main(int argc, char **argv)
{
key_t key = ftok(MSG_PATHNAME, MSG_PRJID); //创建消息队列的key
int msgid = msgget(key, 0666 | IPC_CREAT); //获取消息队列的id
if(msgid < 0)
{
printf("failed to get msgid\r\n");
return -1;
}
struct msgbuf my_msgbuf;
my_msgbuf.mtype = MSG_TYPE; //消息类型标识符
memset(my_msgbuf.mtext, 0, MSGBUF_SIZE); //清空消息缓冲区
while(1)
{
msgrcv(msgid, &my_msgbuf, sizeof(my_msgbuf), MSG_TYPE, 0);
printf("%s", my_msgbuf.mtext);
// sleep(1);
}
return 0;
}
msg_send.c:
#include <sys/types.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include "common.h"
int main(int argc, char **argv)
{
key_t key = ftok(MSG_PATHNAME, MSG_PRJID); //创建消息队列的key
int msgid = msgget(key, 0666 | IPC_CREAT); //获取消息队列的id
if(msgid < 0)
{
printf("failed to get msgid\r\n");
return -1;
}
struct msgbuf my_msgbuf;
my_msgbuf.mtype = MSG_TYPE; //消息类型标识符
memset(my_msgbuf.mtext, 0, MSGBUF_SIZE); //清空消息缓冲区
int cnt = 0;
while(1)
{
sprintf(my_msgbuf.mtext, "Linux is running %d\r\n", cnt++);
msgsnd(msgid, &my_msgbuf, sizeof(my_msgbuf), 0);
printf("sending message\r\n");
sleep(1);
}
return 0;
}
common.h:
#ifndef _COMMON_H_
#define _COMMON_H_
#define MSG_PATHNAME "."
#define MSG_PRJID 1314
#define MSGBUF_SIZE 1024 //消息队列消息缓冲区大小
#define MSG_TYPE 1
/* 消息结构体 */
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[MSGBUF_SIZE]; /* message data */
};
#endif
共享内存
- 效率最高的IPC
- 将某块物理内存多次映射到不同进程的虚拟空间,造成多个进程虚拟内存空间部分重叠,从而能快速交换数据
- 类似共享资源,一般不单独使用,配合信号量、互斥锁等进行临界区保护,防止多进程写入造成数据践踏
共享内存相关API
shmget - allocates a System V shared memory segment //分配System V 共享内存段
//头文件
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
//key —— shm键值
//size —— shm大小
/*
shmflg —— 获取共享内存方式
IPC_CREAT —— 不存在则创建
IPC_EXCL —— 存在则报错
SHM_HUGETLB(since Linux 2.6) —— 使用"大页面"分配shm
SHM_HUGE_2MB、SHM_HUGE_1GB(since Linux 3.8) —— 与SHM_HUGETLB来选择hugetlb page sizes,相应的是2MB、1GB
SHM_NORESERVE(since Linux 2.6.15) —— 不在swap分区中为shm保留内存空间
mode —— shm访问权限
*/
shmat, shmdt - System V shared memory operations
//头文件
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);//映射shm
//shmid —— shm的id
//shmaddr —— 映射内存首地址,通常为NULL让系统自动分配
/*
shmflg —— shm映射方式
SHM_EXEC —— 可以执行shm内容,,但必须拥有执行权限
SHM_RDONLY —— 只读方式映射 无法以只写方式映射
SHM_REMAP —— 重新映射 此时shmaddr不能为NULL
SHM_RND —— 自动选择比shmaddr小的最大页对齐地址
如果shmaddr不为NULL,SHM_RND必须被设置;如果没有设置shmaddr,shmaddr必须为严格的页对齐地址
*/
int shmdt(const void *shmaddr); //解除shm映射
//shmaddr —— shmat映射成功返回的内存地址
RETURN VALUE
成功 shmat() 返回内存地址;失败返回(void *) -1 以及指示错误的errno
成功 shmdt() 返回0; 失败返回 -1, 以及 指示错误errno
shmctl - System V shared memory control//设置shm相关属性
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf); //通常用来删除shm
//shmid —— 共享内存id
/*
cmd —— shm的操作方式;一般为IPC_RMID,删除shm
IPC_STAT —— 获取shm的状态
IPC_SET —— 设置shm的状态
IPC_RMID —— 删除shm
*/
//buf —— 属性信息结构体指针。一般为NULL
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
//更多信息可以查看man手册
shm_recv.c :
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include "common.h"
int main(int argc, char **argv)
{
key_t key = ftok(SHM_PATHNAME, SHM_PRJID); //创建key
int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT); // 获得shmid
char *pbuf = (char *)shmat(shmid, NULL, 0);
memset(pbuf, 0, SHM_SIZE);
char cnt = 0;
while(1)
{
printf(pbuf, "linux message :", cnt++);
sleep(1);
}
shmdt(pbuf); //解除映射
return 0;
}
shm_send.c:
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include "common.h"
int main(int argc, char **argv)
{
key_t key = ftok(SHM_PATHNAME, SHM_PRJID); //创建key
int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT); // 获得shmid
if(shmid < 0)
{
printf("failed to get shmid\r\n");
return -1;
}
char *pbuf = (char *)shmat(shmid, NULL, 0);
memset(pbuf, 0, SHM_SIZE);
char cnt = 0;
while(1)
{
sprintf(pbuf, "linux message : %d\r\n", cnt++);
printf("sending message\r\n");
sleep(1);
}
shmdt(pbuf); //解除映射
return 0;
}
common.h:
#ifndef _COMMON_H_
#define _COMMON_H_
#define SHM_PATHNAME "."
#define SHM_PRJID 1024
#define SHM_SIZE 1024
#endif