线程由于共享资源,可以直接定义全局变量实现通信,但是进程就不行了,接下来主要介绍几种进程间的通信方式。
1.无名管道
- 只能用于具有亲缘关系的进程之间的通信,比如说父子进程,兄弟进程。实际上,文件描述符是一个索引值,系统为每一个进程都维护了一个文件描述符表。也就是说,在不同的进程中打开同一个文件返回的fd未必相等,因此进程间不能通过传递fd的方式来打开同一个文件。而子进程会继承父进程打开的文件和描述符,导致兄弟之间也一样,因此fd可以与文件对应上。所以创建的时候注意一定要先创建管道然后再创建子进程
- 单工的通信方式,一边为读端,一边为写端,一个进程只能读或只能写
无名管道的创建:pipe
int pipe(int fd[2])
fd[2]用于保存文件描述符,fd[0]用于读管道,fd[1]用于写管道,因此一个进程只能用到一个文件描述符
成功的时候返回0,失败的时候返回-1
父进程写管道,子进程读取管道,注意文件描述符需要被关闭两次
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
int main(void)
{
pid_t pid;
char buf[32];
int pfd[2];
if(pipe(pfd) < 0)
{
perror("pipe");
exit(-1);
}
pid = fork();
if(pid == 0)
{
close(pfd[1]); //关闭不用的文件描述符
read(pfd[0], buf, 32);
printf("son recv buf = %s\n",buf);
close(pfd[0]);//关闭不用的文件描述符
exit(0);
}
else
{
close(pfd[0]);//关闭不用的文件描述符
strcpy(buf, "hello my son");
write(pfd[1], buf ,32);
close(pfd[1]);//关闭不用的文件描述符
waitpid(pid, NULL, 0);
exit(0);
}
return 0;
}
- 如果没有读端,向管道里写入数据就会发生管道破裂(进程被信号结束)。可以在创建管道后关闭读端,然后创建子进程,并使用wait函数接收子进程的返回状态,会接收到13信号,也就是管道破裂信号
2.有名管道
- 与无名管道不同的是,有名管道创建出来以后会产生一个实际的管道文件,有路径有名称,不同的进程可以使用路径打开这个文件,也就不存在亲缘关系的限制。
- 读取FIFO文件只能以RDONLY的方式打开,同理写入只能以WRONLY打开
- FIFO文件中的内容被读取以后就消失了
有名管道的创建
int mkfifo(const char *pathname, mode_t mode)
参数为路径和权限, 成功返回0,失败返回-1
示例程序:
写程序:
int main(void)
{
char buf[32] = {0};
int fd;
mkfifo("myfifo",0666)
fd = open("myfifo",O_WRONLY)
fgets(buf, 32, stdin);
write(fd, buf, 32);
close(fd);
return 0;
}
读程序:
int main(void)
{
char buf[32] = {0};
int fd;
fd = open("myfifo", O_RDONLY)
read(fd, buf, 32)
printf("input string length is %ld\n",strlen(buf));
close(fd);
return 0;
}
3.两种管道通信的对比
两种管道都是通过文件来传递信息的,不同的是有名管道产生的是实际的文件,而无名管道的文件是不可见的。因此造成了一些性质上的差异,也就是无名管道只能用于有亲缘关系的进程间,而有名管道就没有这种限制。因为任何进程都可以直接通过路径和文件名打开管道文件,而无名管道只能通过传递文件描述符的形式来打开。
4.信号机制
- 信号机制是在软件层次上对中断机制的一种模拟,Linux内核通过信号通知用户进程,不同的信号类型代表不同的事件。kill -l命令可显示系统当前支持的信号类型。
- 大部分信号 的默认操作是进程终止
- shell中发送信号的命令是kill -号 -pid
1.信号发送
int kill(pid_t pid, int sig);
参数为接收进程的进程号和信号的类型
成功返回0,失败返回-1
2.定时器
int alarm(unsigned int seconds);
参数为定时时间,一个进程中只能设定一个定时器,如果设定了新的定时器,那么原先的就会失效
定时时间到内核会向当前进程发送信号,参数设置为0就是取消当前定时器
成功的时候返回上一个定时器的定时时间,失败返回-1
3.阻塞
int pause(void);
作用为等待某个信号来
使进程进入阻塞,当收到信号以后,先响应信号,如果进程没有结束,则会返回并继续往下执行
接收到信号后返回值为-1
4.设置信号响应方式
void (*signal(int signo, void(*handler)(int)))(int);
参数为处理函数的函数指针和信号类型, 使用宏SIG_IGN代表忽略信号
成功返回原先的信号处理函数
void handler(int signo)
{
if(signo == SIGINT)
puts("I have got SGINT");
if(signo == SIGQUIT)
puts("I have got SGQUIT");
}
int main(void)
{
signal(SIGINT,handler);
signal(SIGQUIT,handler);
while(1)
{
pause();
}
return 0;
}
5.IPC通信
- IPC通信有三种方式,共享内存,消息队列和信号灯集。每个IPC对象都有着唯一的ID和相关联的KEY,ID是系统随机分配的,只有创建它的进程可以直接获得,其他进程只有通过KEY值才能获得ID实现通信。
- shell中可以使用ipcs查看当前系统中存在的IPC对象,ipcrm用来删除IPC对象
- IPC对象创建后会一直存在,需要删除
- IPC通信的流程如下
生成key值
key_t ftok(const char *path, int proj_id);
第一个参数是路径,要求是一个存在且可访问的文件的路径,第二个参数不能为0
成功的时候返回一个合法的key值,失败的时候返回-1
6.共享内存
- 共享内存在内核空间创立,可以被进程映射到用户空间访问,是最为高效的进程间通信方式,进程可以直接读写内存不需要任何数据的拷贝。由于多个进程可以同时访问共享内存,因此需要同步和互斥机制配合使用
- 使用步骤:
1.创建/打开共享内存
int shmget(key_t key, int size, int shmflg);
参数为ftok生成的key值,内存的大小,标志位分为两部分,是否创建和权限。IPC_CREAT|0666,代表存在就创
建,不存在就打开
成功返回共享内存的ID,失败返回-1
2.映射到用户空间
void *shmat(int shmid,const void *shmaddr, int shmflg);
第一个参数为要映射的共享内存ID
第二个参数为映射后的地址,NULL表示系统自动映射
第三个参数为标志位,0表示可读写,SHM_RDONLY表示只读
成功后返回映射后的地址,失败返回(void *)-1
3.读写共享内存
通过指针访问,类型取决于共享内存中存放的数据的类型,接收shmat的返回值
4.撤销映射
int shmdt(void * shmaddr);
参数为映射的首地址
成功返回0,失败返回-1
5.删除对象
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
第一个参数为要操作的共享内存id
第二个参数为要执行的操作,IPC_RMID是删除
第三个参数是一个结构体,如果是删除直接设置为NULL
成功返回0,失败返回-1
- 使用shmctl删除共享内存对象的时候并不是立刻删除,只是标记了要删除。当所有进程都取消了该共享内存的映射并且设置了标记的时候才会真正的删除。而剩下的两种方式使用对应的函数时会立即删除
- 共享内存和管道不一样,读取后内容还在
- 示例代码见信号灯集
7.消息队列
- 使用步骤:
1.打开/创建消息队列
int msgget(key_t key, int msgflag);
第一个参数是ftok函数产生的key值
第二个参数是标志位,可以使用IPC_CREAT|0666
成功返回消息队列的ID,失败返回-1
2.发送消息
int msgsnd(int msgid, const void *msgp, size_t size, int msgflag);
第一个参数是消息队列的id
第二个参数是发送内容缓冲区地址
第三个参数是消息正文的长度,即结构体的长度减去第一个成员的长度
第四个参数是标志位0或者IPC_NOWAIT.表示阻塞和非阻塞。
成功返回0, 失败返回-1
3.定义结构体(消息格式)
进程间通过消息队列通信必须设定消息格式,通过结构体实现,首成员类型必须为long,表示消息类型,为正整
数,其他的自定义,比如:
typedef struct{
long mytype;
char buf[64];
}MSG;
4.接收消息
int msgrcv(int msgid, void *msgp, size_t size,long msgtype, int msgflag);
第一个参数是消息队列的ID
第二个参数是接收消息缓冲区
第三个参数是指定要接收的消息的长度,如果消息内容长一点,那么多出的内容会丢失
第四个参数是消息类型,也就是结构体中的mytype
第五个参数是标志位,0或者IPC_NOWAIT,也就是阻塞和非阻塞
成功返回接收到的消息长度,失败返回-1
5.删除对象
int msgctl(int msgid, int cmd, struct msgid_ds *buf);
第一个参数是对象的ID
第二个参数是具体的操作,IPC_RMID是删除操作
第三个参数是一个结构体,如果是删除直接设置为NULL
示例代码:
clientA.c
typedef struct
{
long mytype;
char buf[64];
}MSG;
#define LEN (sizeof(MSG)-sizeof(long))
#define typeA 100
#define typeB 200
int main(void)
{
MSG msg;
key_t key = ftok(".",'a');
int msgid = msgget(key, IPC_CREAT|0666);
while(1)
{
msg.mytype = typeB;
puts("input >");
fgets(msg.buf, 64, stdin);
msgsnd(msgid, &msg, LEN, 0); //发送消息前设定对方的type
msgrcv(msgid, &msg, LEN, typeA, 0); //接收的是自己的type
printf("recv from B:%s", msg.buf);
}
return 0;
}
clientB.c
typedef struct
{
long mytype;
char buf[64];
}MSG;
#define LEN (sizeof(MSG)-sizeof(long))
#define typeA 100
#define typeB 200
int main(void)
{
MSG msg;
key_t key = ftok(".",'a');
int msgid = msgget(key, IPC_CREAT|0666);
while(1)
{
msgrcv(msgid, &msg, LEN, typeB, 0);
printf("recv from A:%s", msg.buf);
msg.mytype = typeA;
puts("input >");
fgets(msg.buf, 64, stdin);
msgsnd(msgid, &msg, LEN, 0);
}
return 0;
}
8.信号灯集
- 信号灯也叫信号量,上篇文章中介绍的用于线程间同步机制的是Posix无名信号灯,这里讲的是System V 信号灯。同样的,这里的信号灯也代表一种资源的量。前面Posix信号灯的P,V操作都是对某一个信号灯进行操作,这里介绍的是一个信号灯集合,可以对一组信号灯进行操作。
- 在 Linux 上,在相同进程的不同线程之间,则只使用 POSIX 信号量;在进程之间,可以使用 System V 信号量。
- 使用步骤
1.信号灯的打开/创建
int semget(key_t key, int nsems, int semflag);
第一个参数为ftok函数产生的KEY值
第二个参数为信号灯集中信号灯的个数
第三个参数为标志位,一般使用IPC_CREAT|0666
成功时返回信号灯的ID,失败返回-1
2.信号灯的初始化和删除
int semctl(int semid, int semnum, int cmd,...);
第一个参数semid表示要操作的信号灯集id
第二个参数表示要操作的集合中的信号灯编号,每个信号灯都要单独操作
第三个参数表示具体的操作,SETVAL表示初始化,IPC_RMID表示删除
第四个参数是否存在取决于第三个参数,如果是初始化,则要用到,是一个共用体union semun,其中含有一个
val成员用来设置要初始化的值
成功时返回0,失败返回-1
3.信号灯的P/V操作,如果有多个信号灯则需要每个都操作一遍
int semop(int semid, struct sembuf *sops, unsigned nsops);
第一个参数为要操作的信号灯集ID
第二个参数是一个描述对某一个信号灯操作的结构体,如果要对多个信号灯操作就是结构体数组
第三个参数是要操作的信号灯的个数
成功时返回0,失败返回-1
sembuf结构体:
struct sembuf
{
short semnum; //信号灯编号,从0开始
short sem_op; //负数代表P操作,正数代表V操作
short sem_flag; //0或者IPC_NOWAIT
};
信号灯更多的是实现对共享资源的访问的控制(同步,互斥),示例如下:
父子进程通过信号灯同步对共享内存的读写,其中父进程写入字符串到共享内存,子进程打印,父进程输入quit后删除共享内存和信号灯集合,程序结束
#define N 64
#define READ 0 //代表可读的缓冲区,信号灯编号为0
#define WRITE 1 //代表可写的缓冲区,信号灯编号为1
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
void pvfunc(int semid, int num, int op) //自定义的信号灯PV操作函数
{
struct sembuf buf;
buf.sem_num = num;
buf.sem_op = op;
buf.sem_flg = 0;
semop(semid, &buf, 1);
}
int main(void)
{
int shmid, semid, s[] = {0,1};
union semun myun;
pid_t pid;
key_t key;
char * shmaddr;
key = ftok("./a.txt",'s');
shmid = shmget(key, N, IPC_CREAT|0666); //创建共享内存
if((semid = semget(key, 2, IPC_CREAT|0666)) < 0) //创建信号灯集
{
perror("semget");
goto error1; //创建信号灯集失败要删除已经创建的共享内存
}
for(int i = 0; i < 2; i++)
{
myun.val = s[i];
semctl(semid, i, SETVAL, myun); //对信号灯集中的每个信号灯进行初始化
}
if((shmaddr = (char *)shmat(shmid,NULL,0)) == (char *)-1)
{
perror("shmat");
goto error2; //映射到用户空间失败要删除创建好的信号灯集
}
pid = fork();
if(pid == 0) //子进程打印父进程输入的字符串
{
while(1)
{
pvfunc(semid, READ, -1); //先检查有没有可读的缓冲区,执行P操作
printf("%s\n",shmaddr);
pvfunc(semid, WRITE, 1); //访问完后执行V操作
}
}
else //父进程负责输入字符串
{
while(1)
{
pvfunc(semid, WRITE, -1); //先检查有没有可写的缓冲区,执行P操作
puts("input >");
fgets(shmaddr, N, stdin);
if(strcmp(shmaddr, "quit\n") == 0)
break;
pvfunc(semid, READ, 1); //访问完后执行V操作
}
kill(pid, SIGUSR1); //杀死子进程
}
error2:
semctl(semid, 0, IPC_RMID);
error1:
shmctl(shmid, IPC_RMID, NULL);
return 0;
}