进程间通信(IPC,InterProcess Communication)是指在不同进程之间交换信息。
IPC的方式有管道(包括命名管道和非命名管道)、消息队列、共享内存、信号、信号量、Socket等。
一、管道
管道通常指无名管道、是Unix系统最古老的形式。
1、特点
1.半双工,具有固定的读写端
2.只能用于具有亲缘关系的进程间通信(如父子,兄弟)。
3.它可以看成一种特殊的普通文件,可以使用write、read等函数对其进行操作,但是其只存在于内存中。
2、函数原型如下
#include <unistd.h>
int pipe(int pipefd[2]);//调用失败返回-1,并设置errno,调用成功,返回0
//pipefd[0]表示读,pipefd[1]表示写,最后不需要管道时,用
close()关闭即可
//当用read对管道进行读取时,若管道中无数据,则阻塞,若写端已关闭,并且读的数据为0,说明已读完
例程如下:
#include <stdio.h>
#include <unistd.h>
int main(){
int fd[2];
pid_t pid;
char buf[20];
if(pipe(fd) < 0){//创建管道
perror("pipe:");
}
if((pid = fork()) < 0){//创建子进程
perror("fork");
}
else if(pid > 0){
close(fd[0]);//关闭读口
write(fd[1],"hello world\n",12);//通过写口向管道中写数据
}
else{
close(fd[1]);//关闭写口
read(fd[0],buf,20);//从管道中读数据
printf("%s",buf);
}
return 0;
}
输出如下
二、FIFO
FIFO,也称命名管道
1、特点
1.FIFO可以在无亲缘关系的进程间进行数据交换。
2.FIFO以一种特殊设备文件形式存在于文件系统中。
3.半双工
2、函数原型
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);//成功返回0,出错返回-1并置位errno
//其中mode与open()函数中的可选项一致,可以以一种普通文件IO操作它
可设置为阻塞和非阻塞(O_NONBLOCK)两种方式。
若没有设置(O_NONBLOCK)(默认),只读要阻塞到某个进程为写而打开此文件,只写要阻塞到某个进程为读而打开此文件。
若设置了(O_NONBLOCK),只读立即返回,而只写将出错返回-1,如果没有进程为读而打开该FIFO,其errno置ENXIO。
常用阻塞方式。
在读数据时,FIFO管道中同时清除数据,并且先进先出。
例程如下:
write_fifo.c
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<string.h>
#include<unistd.h>
int main(){
int fd;
char buf[1024] = "hello world";
if((fd = open("./fifo",O_WRONLY)) < 0){
perror("FIFO");
}
for(int i =0; i < 5;i++){
write(fd,buf,sizeof(buf));
sleep(1);
}
close(fd);
return 0;
}
read_fifo.c
#include <stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<unistd.h>
#include<string.h>
int main()
{
int fd;
char buf[1024];
int n;
if(mkfifo("./fifo",0666) < 0 && errno != EEXIST){
perror("Creat FIFO failed");
}
if((fd = open("./fifo",O_RDONLY)) < 0){
perror("Open FIFO Failed");
}
for(int i =0 ;i < 5;i++){
read(fd,buf,1024);
printf("%s\n",buf);
}
close(fd);
return 0;
}
输出如下
三、消息队列
消息队列,是一种链表,存放在内核中。一个消息队列由一个标识符(即ID)来标识。
1、特点
1.消息队列是面向记录的,其中的消息具有特定的格式和特定的优先级。
2.在进程结束后,消息队列及其内容不会被删除。
3.消息队列可以实现消息的随机查询,消息不一定以先进先出的顺序读取,可以按消息的类型读取。
2、原型
int msgget(key_t key, int msgflg);//创建或打开一个消息队列
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);//添加信息成功返回0,失败返回-1
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);//读取信息,成功返回接受信息的长度
int msgctl(int msqid, int cmd, struct msqid_ds *buf);//控制消息队列,成功返回0,失败返回-1
key:标识特定的消息队列,用来确保可以在不同的进程中访问同一个消息队列。
msgflag:标志位
msqid:消息队列ID号
msgp:指向要发送(接收)的信息地址
msgtyp:0,返回队列中第一个消息。大于0,返回队列中消息类型为type的第一个消息。小于0,返回消息队列中小于或等于type绝对值的消息,如果有多个,取类型最小的消息。
msgsz:发送(接收)信息的大小
msgrcv会发生阻塞,而msgsnd不会发生阻塞
以下两种情况将新建一个消息队列:
若没有与key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
key值为IPC_CREAT。
3、示例如下
queenSend.c
#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(){
key_t key;
int msgId;
struct msgbuf sendBuf = {888,"hello world"};
key = ftok(".",4);
printf("key = %d\n",key);
if((msgId = msgget(key,IPC_CREAT | 0777)) == -1){
perror("");
}
printf("msgId = %d\n",msgId);
if(msgsnd(msgId,&sendBuf,sizeof(sendBuf.mtext),0) == 0){
perror("");
}
return 0;
}
queenreceive.c
#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
struct msgbuf{
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main(){
key_t key;
int msgId;
struct msgbuf receiveBuf;
key = ftok(".",4);
printf("key = %d\n",key);
msgId = msgget(key,IPC_CREAT | 0777);
printf("msgId = %d\n",msgId);
msgrcv(msgId,&receiveBuf,sizeof(receiveBuf.mtext),888,0);
printf("%s",receiveBuf.mtext);
return 0;
}
输出为:
在用msgget()的时候要注意,一旦对队列的访问权限赋值以后,下次使用时也应是这样的权限,否则无法正常使用。如果想知道自己是否创建了队列,使用ipcs -q即可查看目前存在的队列。
struct msgbuf{
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
mytype代表的是发送消息的类型,用来在链表中找到mtext。一般为正整数即可。
四、共享内存
1、特点
1.共享内存是访问速度最快的IPC
2.因为多个进程可以同时操作,所以需要同步
3.信号量+共享内存通常一起使用,信号量用来同步对共享内存的访问。
2、原型
int shmget(key_t key, size_t size, int shmflg);//创建或获取一个共享内存,成功返回内存ID,失败返回-1
void *shmat(int shmid, const void *shmaddr, int shmflg);//用来链接共享内存,成功返回共享内存的地址,失败返回-1
int shmdt(const void *shmaddr);//断开与共享内存的连接,成功返回0,失败返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);//控制共享内存的相关信息,成功返回0,是被返回-1
key:和消息队列的可以是一样使用的。
size:必须是M的倍数
shmflag:一般为0
buf:返回的信息值
使用shmget创建一段共享内存后,必须指定其size;而如果使用已经存在的共享内存,则将size设置为0(不设为0,可能无法正常读取)。
创建一段共享内存后,它其实与进程之间是没有关系的,需要使用shmat链接,链接成功后,就可以直接访问。
shmdt是用来断开链接的,但是共享内存仍然存在。
shmctl是对共享内存执行多种操作,根据cmd,最常见的是IPC_RMID(移除)。
3、示例如下:
shmSend.c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <time.h>
int main(){
int shmid;
key_t key;
char *shmaddr;
key = ftok(".",1);
shmid = shmget(key,1024*2,IPC_CREAT | 0666);
printf("shmid = %d",shmid);
shmaddr = shmat(shmid,0,0);
strcpy(shmaddr,"hello world\n");
sleep(5);
shmdt(shmaddr);
shmctl(shmid,IPC_RMID,0);
return 0;
}
shmRece.c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <time.h>
int main(){
int shmid;
key_t key;
char *shmaddr;
key = ftok(".",1);
shmid = shmget(key,1024 * 2,0);
printf("shmid = %d",shmid);
shmaddr = shmat(shmid,0,0);
printf("%s",shmaddr);
sleep(5);
shmdt(shmaddr);
return 0;
}
五、信号
Linux中信号有很多,我们可以通过kill -l查看。
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
信号从1开始编号,不存在0信号。
信号的处理有三种方法:忽略,捕捉和默认动作。
忽略信号:顾名思义,就是不对信号进行任何处理,但是有两种信号不能被忽略(SIGKILL和SIGSTOP),以确保进行进程不会变为无法管控的程序。
捕捉信号:需要编写处理程序对信号进行处理。
默认信号:用户没有对信号的行为进行编写,捕捉到信号后就执行其默认的动作。可以通过man 5 siganl查看默认动作。
函数原型
信号接收函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
这两个函数是信号接收函数,signal()有两个参数,第一个是发送的信号类型,第二个是处理信号的函数指针。sigaction()有三个参数,第一个参数为接受到的信号类型,第二个为处理信号的结构体,第三个信号是一个备份,一般设置为NULL即可。具体如下图
最后的info如下
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 Linux 2.6.32) */
void *si_call_addr; /* Address of system call instruction
(since Linux 3.5) */
int si_syscall; /* Number of attempted system call
(since Linux 3.5) */
unsigned int si_arch; /* Architecture of attempted system call
(since Linux 3.5) */
}
可以只配置需要的。
信号发送函数
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
int sigqueue(pid_t pid, int sig, const union sigval value);
kill()函数只需设置发送的pid号,和信号类型即可。
sigqueue()前两个参数也是这样,最后一个参数为联合体,定义如下。
union sigval {
int sival_int;
void *sival_ptr;
};
例程如下
我们使用sigqueue()发送信号,使用sigaction()接收信号。
sigre.c
#include<stdio.h>
#include<signal.h>
void deal(int pid, siginfo_t *info, void *mtext){
printf("signum = %d\n",pid);
if(mtext != NULL){
printf("info.avl = %d\n",(info->si_int));
}
}
int main(){
struct sigaction act;
printf("pid = %d\n",getpid());
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = deal;
sigaction(SIGUSR1,&act,NULL);
while(1);
return 0;
}
sigsend.c
#include<stdio.h>
#include<signal.h>
#include<string.h>
int main(int argc,char **argv){
union sigval val;
val.sival_int = 100;
int sig = atoi(argv[1]);
int pid = atoi(argv[2]);
sigqueue(pid,sig,val);
return 0;
}
输出为:
六、信号量
信号量(semaphore)与已经介绍过的IPC不同,它是一个计数器。信号量用于实现进程间的同步,而不是用于进程间通信。
1、特点
1.信号量用于进程间的同步,若要在进程间传递数据,需要结合其它方式
2.信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作
3.每次对信号量的PV操作,不仅限于对信号量值+1或-1,可以加减任意正整数。
4.支持信号量组
5、最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二值信号量。但页存在可以取多个正整数的信号量,被称为通用信号量。
2、函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);//获取或创建一个信号量,成功返回信号量集ID,失败返回-1
int semop(int semid, struct sembuf *sops, size_t nsops);//对信号量组进行操作,成功返回0,失败返回-1
int semctl(int semid, int semnum, int cmd, ...);//控制信号量的相关信息
3、代码如下
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.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 id){//拿走信号量
struct sembuf set;
set.sem_num = 0;//设置的是第几个信号量
set.sem_op = -1;//信号量的值-1
set.sem_flg = SEM_UNDO;//阻塞
semop(id,&set,1);
printf("getkey\n");
}
void vPutBackKey(int id){//放回信号量
struct sembuf set;
set.sem_num = 0;
set.sem_op = +1;
set.sem_flg = SEM_UNDO;
semop(id,&set,1);
printf("put back the key\n");
}
int main(){
key_t key;
int semid;
key = ftok(".",1);
semid = semget(key,1,IPC_CREAT|0666);//key的含义与之前相同,1代表信号量集里面只有一个信号,flag是创建信号量并赋予权限
union semun initsem;//用联合体定义一个变量
initsem.val = 0;//信号量初值为0,需要置1,后面程序才能继续执行
semctl(semid,0,SETVAL,initsem.val);//创建或打开一个信号量
int pid = fork();
if(pid > 0){
pGetKey(semid);//想要得到信号量,但是信号量为0,不能执行
printf("this is father\n");
vPutBackKey(semid);//放回信号量
}
else if(pid == 0){
printf("this is children\n");
vPutBackKey(semid);//放回信号量
}
else{
perror("");
}
return 0;
}
输出如下
七、Socket
1、特点
1.主要用与两个不同的主机之间通信
2.TCP和UDP都是Socket,不同的通信方式
2、端口号作用
一台拥有IP地址的主机可以提供许多服务,比如Web服务,FTP服务,SMTP服务等。但是只有一个IP地址,这个时候就需要端口号,来区分不同的服务。服务器一般都是通过端口号识别服务的。
3、字节序
这就是单片机中常说的大端模式和小端模式。x86CPU都是小端字节序。TCP/UDP/IP都是大端模式,所以在传输时,需要转换。
字节序转换常用API如下:
#include <netinet/in.h>
uint16_t htons(uint16_t host16bitvalue); //返回网络字节序的值
uint32_t htonl(uint32_t host32bitvalue); //返回网络字节序的值
uint16_t ntohs(uint16_t net16bitvalue); //返回主机字节序的值
uint32_t ntohl(uint32_t net32bitvalue); //返回主机字节序的值
h代表host,n代表net,s代表short(两个字节),l代表long(4个字节),通过上面的4个函数可以实现主机字节序和网络字节序之间的转换。有时可以用INADDR_ANY,INADDR_ANY指定地址让操作系统自己获取,一般来说,IP是long,端口是short
4、Socket服务器和客户端的开发步骤
我们可以将Socket当做一个文件,主机首先创建一个Socket,然后用bind对其进行配置,最后开始监听,将相当于创建了一个线程,一直监听,直到有客户端接入,接受,创建一个新的文件,对其进行读写就是交互。
5、函数原型如下
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);//addrlen:结构体大小
int listen(int sockfd, int backlog);//back:指定在请求队列中允许的最大请求数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
而sockaddr如下,经常用下面那个:
6、示例代码如下
socketsever.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
//#include<linux/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<string.h>
#include <unistd.h>
int main(){
int s_fd;
char readBuf[128];//接收到的数据
char *msg = "I get your message";//发送出的数据
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
memset(&s_addr,0,sizeof(struct sockaddr_in));//置零
memset(&c_addr,0,sizeof(struct sockaddr_in));//置零
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(8988);//转换端口号
inet_aton("127.20.10.3",&s_addr.sin_addr);//转换IP地址
//1.sock
s_fd = socket(AF_INET,SOCK_STREAM,0);
if(s_fd == -1){
perror("socket");
}
printf("s_fd = %d\n",s_fd);
//2.bind
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10);//开始监听
//4.accept
int clen = sizeof(struct sockaddr);
int c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);//会阻塞到这里
if(c_fd == -1){
perror("accept");
}
printf("get connect:%s\n",inet_ntoa(c_addr.sin_addr));
//5.read
int nread = read(c_fd,readBuf,128);
if(nread == -1){
perror("read");
}
else{
printf("get message:%d,%s\n",nread,readBuf);
}
//read(c_fd,);
//6.write
write(c_fd,msg,strlen(msg));
while(1);
return 0;
}
socketclient.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
//#include<linux/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<string.h>
#include <unistd.h>
int main(){
int c_fd;
char readBuf[128];
char *msg = "mseeage from client";
struct sockaddr_in c_addr;
memset(&c_addr,0,sizeof(struct sockaddr_in));
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(8988);
inet_aton("127.20.10.3",&c_addr.sin_addr);
//1.sock
c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd == -1){
perror("socket");
}
//2.connect
if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr)) == -1){//请求连接
perror("connect");
}
//3.write
write(c_fd,msg,strlen(msg));
//4.read
int nread = read(c_fd,readBuf,128);
if(nread == -1){
perror("read");
}
else{
printf("get message from server:%d,%s\n",nread,readBuf);
}
return 0;
}
输出如下:
服务端
客户端
在写完服务端后,可以使用telnet检测是否可以正常连接:如telnet 127.20.10.3 8988