进程间的5种通信包括:管道(命名管道和无名管道)、消息队列、信号量、共享内存、Socket、Streams等。其中socket和streams支持不同主机之间的IPC。
一、无名管道
1、特点
a:管道是半双工的,有固定的读端和写端。父进程通过管道和子进程通信时,父进程通过写端向管道中写数据,子进程通过读端从管道中读取数据;子进程通过写端向管道中写数据,父进程通过读端从管道中读取数据。这两个过程不能同时进行,数据只能在一个方向上流动。
b:管道类似于水管,水管中的水流完就没了,管道中的数据,传输完也会丢失。它可以看成一种特殊的文件,可以用read、write来操作,但不是普通的文件,不属于任何文件系统,只存在于内存中。
c:只能用于具有亲缘关系的进程之间的通信(父子进程或兄弟进程之间)。
d:关掉管道关掉读端跟写端就可以完成。
2、函数原型
#include <unistd.h>
int pipe(int pipefd[2])
参数是一个整型数组,代表两个文件描述符,pipefd[0]表示为读而打开,pipefd[1]表示为写而打开,即读数据要操作pipefd[0],写数据要往pipefd[1]里面写
返回值:成功返回0,失败返回-1
3、demo实例
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int fd[2];
char *readBuf = (char*)malloc(sizeof(char)*128);
pid_t pid;
if(pipe(fd)==-1)
{
printf("pipe failed\n");
}
pid = fork();//创建进程
if(pid<0)
{
printf("fork failed\n");
}
else if(pid>0)
{
//父进程关掉读端,通过写端往管道里面写内容
close(fd[0]);
write(fd[1],"hello pipe",strlen("hello pipe"));
}
else
{
//子进程关掉写端,通过读端从管道里读取内容
close(fd[1]);
read(fd[0],readBuf,128);
printf("read:%s\n",readBuf);
}
return 0;
}
执行结果:
read:hello pipe
二、FIFO(有名管道)
1、特点
a:与无名管道不同它可以在无关进程间交换数据
b:有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中
2、函数原型
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数一:管道名是一个文件,后面的后面的通信通过这个文件进行读写操作
参数二:与open函数的mode相同,一旦创建了FIFO就可以用一般的文件I|O来操作它
返回值:成功返回0,失败返回-1
3、demo实例
//read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>//errno头文件
#include <stdlib.h>
#include <fcntl.h>
int main()
{
int fd ;
char *readBuf = (char *)malloc(sizeof(char)*128);
if(mkfifo("./file",0600)==-1 && errno!=EEXIST)
{
printf("mkfifo failed\n");
}//以可读可写0600方式创建管道,管道存在也会创建失败,所以mkfifo返回值=-1的同时,需满足管道不存在,管道才是真正的创建失败
fd = open("./file",O_RDONLY);
//以只读阻塞方式打开管道,会阻塞到write函数向管道中写入数据才会停止阻塞
if(fd ==-1)
{
printf("open fifo failed\n");
}
int n_read = read(fd,readBuf,128);
printf("read context is %s size is %d\n",readBuf,n_read);
close(fd);
return 0;
}
//write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd;
char *writeBuf = "xiaobian henshuai!";
fd = open("./file",O_WRONLY);
if(fd==-1)
{
printf("open fifo failed\n");
}
write(fd,writeBuf,strlen(writeBuf));
close(fd);
return 0;
}
当open一个fifo时,如果没有设置非阻塞标志(默认),open只读方式打开fifo会阻塞到某个进程为写而打开此fifo,若设置了非阻塞标(O_NONBLOCK),则只读open立即返回,只写open将出错返回-1,一般都以阻塞方式打开。
有名管道实际上就是创建了一个管道文件,通过open、write、read函数对文件进行操作,完成进程间通信。
三、消息队列
消息队列有自己的标识符(队列ID),存放于Linux内核中,消息队列链表如何管理,由内核处理。
1、特点
a:消息队列是面向记录的,其中的消息有特定的格式特定的优先级
b:消息队列独立于发送接收进程。进程终止时,消息队列及其内容不会消失。
c:消息队列可以实现消息的随机查询,消息队列不一定要以先进先出的次序读取,也可以按消息的类型读取。
2、函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//创建或打开消息队列,成功返回队列id,失败返回-1
int msgget(key_t key, int msgflg);
参数key:键值消息队列索引号,可自行设定也可动态获取
参数msgflag:创建或打开队列的方式,通常还加上消息队列的权限
//添加消息,成功返回0,失败返回-1
int msgsnd(int msqid, const void *msgp, size_t msgsz, int flg);
参数msgid:消息队列ID
参数const void *msgp :要发送的消息,是一个结构体
参数msgsz:消息的大小
参数flg:通常为0
//读取消息,成功返回消息数据长度,失败返回-1
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int flg);
参数void *msgp :要接受的消息,存放在一个结构体中
参数msgtyp:消息队列的类型
参数flg:设为0,当为默认,非阻塞的方式
//控制消息队列,成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数cmd:通过man手册查询,有相应的宏,IPC_STAT IPC_RMID IPC_SET
参数buf:指向共享内存模式和访问权限的结构,我们不关心它,通常为NULL
以下两种情况将msgget创建一个消息队列
没有与键值key对应的消息队列,并且msgflg标志位包含了IPC_CREAT
key的参数为IPC_PRIVATE
以下两种方式获取键值key
直接写死,0x520
动态获取,ftok(".",1) (具体原因自行baidu哦)
3、demo实例
//msgsnd.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>
//每一个消息都有自己的类型和大小用结构体定义消息
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main()
{
int msgid;
key_t key;
key = ftok(".",1);
if(key==-1)
{
printf("key create failed\n");
exit(-1);
}
struct msgbuf sendbuf={888,"msgsnd give you a msg"};
struct msgbuf readbuf;
msgid=msgget(key,IPC_CREAT|0777);//创建消息队列的同时给它可读可写可执行的权限
if(msgid==-1)
{
printf("open msg que falied\n");
}
//添加一个消息类型为888的消息
msgsnd(msgid,&sendbuf,strlen(sendbuf.mtext),0);
//等待接受一个消息类型为999的消息
msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),999,0);
printf("%s\n",readbuf.mtext);
//将消息队列msgid从内核中移除
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
//msgrcv.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main()
{
int msgid;
key_t key;
key = ftok(".",1);
if(key==-1)
{
printf("key create failed\n");
exit(-1);
}
struct msgbuf readbuf;
struct msgbuf sendbuf = {999,"msgrcv had get your msg"};
msgid = msgget(key,IPC_CREAT|0777);
if(msgid==-1)
{
printf("msg create failed\n");
}
//等待接收一个消息类型为888的消息
msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),888,0);
printf("%s\n",readbuf.mtext);
//添加一个消息类型为999的消息
msgsnd(msgid,&sendbuf,strlen(sendbuf.mtext),0);
//将消息队列msgid从内核中移除
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
四、共享内存
共享内存就是允许两个或多个进程共享一定的存储区,数据直接写到这块内存,通过指针访问这块内存,完成IPC
1、特点
a:两个进程地址映射到同一片物理地址完成通信
b:共享内存是传输速度最快的通信方式,客户进程和服务进程传递的数据直接从内存里存取、放入,数据不需要在两进程间复制
c:共享内存的数据的也没有什么限制
2、函数原型
#include <sys/ipc.h>
#include <sys/shm.h>
//创建或获取一个共享内存,成功返回共享内存ID,失败返回-1
int shmget(key_t key, size_t size, int shmflg);
参数key:系统建立IPC通讯(消息队列、信号量和共享内存)时必须指定一个ID值,通常用ftok获取
参数size:共享内存的大小,通常兆的整数倍
参数shmflg:是一组标志,创建一个新的共享内存,将shmflg 设置了IPC_CREAT标志后,共享内存存在就打开。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的共享内存,如果共享内存已存在,返回一个错误。一般我们会还或上一个文件权限
//连接共享内存到当前的地址空间,成功返回指向共享内存的指针,失败返回-1
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数shmid:shmget函数返回的共享内存标识id
参数shmaddr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址
参数shmflg:shm_flg是一组标志位,通常为0
//断开与共享内存之间的连接,成功返回0,失败返回-1
int shmdt(const void *shmaddr);
参数shmaddr:是以前调用shmat时的返回值
//控制共享内存的相关信息,成功返回0,失败返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数shmid:shmget函数返回的共享内存标识id
参数cmd:,cmd是要采取的操作,它可以取下面的三个值 :
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID:删除共享内存段
参数 buf:buf是一个结构指针,它指向共享内存模式和访问权限的结构,我们不关心它,通常为NULL
3、demo实例
//shmsend.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <string.h>
int main()
{
key_t key;
key = ftok(".",1);
int shmid;
void *shmaddr =NULL;
shmid = shmget(key,1024*4,IPC_CREAT|0600);
if(shmid ==-1)
{
printf("shmid failed\n");
exit(-1);
}
shmaddr = shmat(shmid,0,0);
if(shmaddr==NULL)
{
printf("shmaddr failed\n");
exit(-1);
}
strcpy(shmaddr,"xiaobian henshuai");//往共享内存中添加数据
printf("send msg over\n");
sleep(5);//等待进程读取共性内存中的数据
shmdt(shmaddr);//断开连接
shmctl(shmid,IPC_RMID,0);//删除共享内存
return 0;
}
//执行结果:send msg over
//shmreceive.c
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
int main()
{
key_t key;
key = ftok(".",1);
char *shmaddr=NULL;
int shmid;
shmid = shmget(key,4*1024,0);//共享内存已经在上一个demo中创建了,不用重复创建
if(shmid ==-1)
{
printf("shmid failed\n");
exit(-1);
}
shmaddr=shmat(shmid,0,0);
printf("%s\n",shmaddr);
printf("receive msg over\n");
shmdt(shmaddr);
shmctl(shmid,IPC_RMID,0);
return 0;
}
//执行结果:receive msg over
// xiaobian henshuai
关于共享内存的指令:
ipcs -m 查看共享内存
ipcrm -m 共享内存id号(shmid) 删除共享内存
五、信号
1、信号概述
信号是Linux系统中用于进程之间通信或操作的一种机制,对Linux来说信号是一种软中断,许多重要程序都需处理信号,信号为Linux提供了一种处理异步中断的方法。
用 kill -l 可以查看系统中信号的序号和名字
2、信号的三种处理方法
捕捉、忽略和默认动作
捕捉:类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理,当信号产生时,由内核调用用户自定义的函数,以此来实现对信号的处理。
忽略:忽略某个信号,对该信号不做任何处理,但SIGKILL和SIGSTOP两种信号不能被忽略。
默认动作:每个信号系统都有对应的默认处理动作,对该信号的处理保留系统的默认值,大多都是使进程终止。
对信号的操作可以使用kill指令发送信号
kill -9 进程号 //杀死该进程的信号
3、函数原型
//信号处理函数的注册
//初级:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
//高级:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
//信号处理函数的发送
//初级:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig)
//高级:
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
4、demo实例信号初级版
//signal.c
#include <stdio.h>
#include <signal.h>
void handler(int signum)
{
switch (signum)
{
case SIGUSR1:
printf("SIGUSR1 is catched\n");
break;
case SIGUSR2:
printf("SIGUSR2 is catched\n");
break;
}
}
int main()
{
signal( SIGUSR1,handler);//对SIGUSR1 信号进行捕捉,指定handler作为其处理函数
signal(SIGUSR2,handler);
signal(SIGINT,SIG_IGN);//将Ctrl c 忽略
while(1);
return 0;
}
ps -aux |grep a.out //查找a.out的进程号id
kill -10 进程号
kill -12 进程号
执行结果:
SIGUSR1 is catched
SIGUSR2 is catched
//kill.c
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc ,char**argv)
{
if(argc!=3)
{
printf("params error\n");
exit(-1);
}
int signum;
int pid;
signum = atoi(argv[1]);//将ascll码转换成整型数
pid =atoi(argv[2]);
kill(pid,signum);
return 0;
}
./a.out //运行信号注册可执行性程序
ps -aux |grep a.out //查找a.out的进程号id
./kill 12 进程号
//在两个终端种分别进行
执行结果:
SIGUSR2 is catched
//kill.c 的补充用system实现
#include <stdio.h>
int main(int argc,char**argv)
{
int signum;
int pid;
char cmd[128];
signum = atoi(argv[1]);
pid = atoi(argv[2]);
sprintf(cmd,"kill -%d %d",signum,pid);//动态的构造字符串的函数
system(cmd);
return 0;
}
5、demo实例信号高级版,信号携带消息
//sigaction 函数介绍
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
参数一:要捕获的信号
参数二:一个结构体
参数三:输出先前信号的处理方式,不需要可设为NULL
结构体介绍:
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数。
sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置,默认为0,可不做要求
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用:
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
sa_sigaction 另一个信号处理函数,它有三个参数
参数一:捕获的信号
参数二是一个结构体
参数三:是一个指针,为NULL表明没有数据,否则有数据
结构体介绍:存放捕获的信号中携带的消息
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since kernel 2.6.32) */
}
//sigqueue函数介绍
int sigqueue(pid_t pid, int sig, const union sigval value);
参数一:进程ID
参数二:信号类型
参数三:是一个联合体
union sigval {
int sival_int;
void *sival_ptr;
};
//sigget.c
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int signum, siginfo_t * info, void * context)
{
if(context!=NULL)//指针不为空才有数据
{
printf("send pid =%d\n",info->si_pid);//发送信号进程的pid
printf("received value =%d\n",info->si_int);//发送信号携带的整形数据
printf("received value =%d\n",info->si_value.sival_int);//发送信号携带的整形数据
}
}
int main()
{
printf("get pid is %d\n",getpid());
struct sigaction act;
act.sa_sigaction = handler;
act.sa_flags =SA_SIGINFO;
sigaction(SIGUSR1,&act,NULL);//捕获信号SIGUSR1 并用sa_sigaction = handler处理函数,获取信号携带的信息
while(1);//等待捕获信号
return 0;
}
//sigput.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc,char **argv)
{
if(argc!=3)
{
printf("params error\n");
exit(-1);
}
printf("send pid is %d\n",getpid());
union sigval value;//信号携带的信息为一个联合体,可以存放整型数据,也可以字符串
value.sival_int = 100;
int signum = atoi(argv[1]);
int pid = atoi(argv[2]);
sigqueue(pid,signum,value);//给进程 pid 发送SIGUSR1信号 携带value信息
return 0;
}
六、信号量
信号量与前面介绍的IPC结构不同,是一个计数器,用于实现进程间的互斥与同步,而不是用于存储进程间的通信数据
1、特点
a:信号量用于进程间同步,若要在进程间传递数据要结合共享内存
b:信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作
2、函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
//创建或获取一个信号量组,成功返回信号量组id,失败返回-1
int semget(key_t key, int nsems, int semflg);
参数一:key是长整型(唯一非零),系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到
参数二:nsem指定信号量集中需要的信号量数目,它的值几乎总是1
参数三:flag是一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作。
//对信号量组进行操作,改变信号量的值,成功返回0,失败返回-1
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数三:nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作
参数二结构体:
struct sembuf
{
short sem_num; //除非使用一组信号量,否则它为0
short sem_op; //信号量在一次操作中需要改变的数据,通常是两个数
//一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量,
//并在进程没有释放该信号量而终止时,操作系统释放信号量 };
//控制信号量的相关信息
int semctl(int semid, int semnum, int cmd, ...);
参数一:由semget返回的信号量标识符
参数二:要操作当前信号量集的哪一个信号量
参数三:cmd通常是下面两个值中的其中一个
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
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) 使用的缓存区*/
};
3、demo实例
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.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) */
};
void pGetKey(int semid)//p操作相当于拿锁的功能
{
struct sembuf sem;
sem.sem_num = 0;
sem.sem_op =-1;//拿锁 锁的数量减1
sem.sem_flg = SEM_UNDO;
semop(semid,&sem,1);
}
void vPutKey(int semid)//V操作 相当于放锁的功能
{
struct sembuf sem;
sem.sem_num = 0;
sem.sem_op =1;//放锁 锁的数量加1
sem.sem_flg = SEM_UNDO;
semop(semid,&sem,1);
}
int main()
{
key_t key;
key = ftok(".",1);
int semid;
union semun semval;
semval.val=0;//刚开始没有锁
semid=semget(key,1,IPC_CREAT|0600);
if(semid==-1)
{
printf("sem error\n");
exit(-1);
}
semctl(semid,0,SETVAL,semval);
pid_t pid = fork();
if(pid>0)
{
pGetKey(semid);//拿锁
printf("this is father\n");
vPutKey(semid);//放锁
semctl(semid,0,IPC_RMID);//释放锁
}
else if(pid==0)
{
printf("this is child\n");
vPutKey(semid);//放锁
}
return 0;
}
代码解析:fork一个子进程,不能保证,父子进程哪一个先运行,此时设置锁的数量为0,让父进程去拿锁,拿不到,只能子进程先运行,子进程运行完,进行v操作放锁,这时父进程才能拿锁,执行相关程序。