常见的IPC方式有管道、命名管道、存储映射、本地套接字、信号。
一、管道
管道是种简单的进程间通信方式,作用于父子进程或有血缘关系的进程之间,通过调用pipe函数创建管道。
管道的特性 :
1、只能作用于血缘进程之间;
2、采用消息队列机制,数据一旦读走就不存在;
3、一端读一端写,数据只能从写端流向读端,如果想双向通信,只能再建立一个管道;
4、其本质是伪文件,是内核种的一块缓冲区;
创建管道函数:
int pipe(int pipefd[2]); 成功:0;失败:-1,设置 errno;
1 #include<stdio.h> 2 #include<string.h> 3 #include<stdlib.h> 4 #include<unistd.h> 5 int main(){ 6 int ret; 7 int fd[2]; 8 pid_t pid; 9 char buf[1024]; 10 //创建管道 成功返回0,失败返回-1,设置errno; 11 ret = pipe(fd); 12 if(ret==-1){ 13 perror("pipe is error"); 14 exit(1); 15 } 16 pid =fork(); 17 if(pid==0){ 18 //子进程 19 sleep(1); 20 close (fd[0]); //管道规定一端读另一端写,子进程关闭读端,子进程写数据 21 write(fd[1],"hello pipe",strlen("hello pipe")); 22 close (fd[1]); 23 } 24 else if(pid >0){//父进程 25 close (fd[1]);//关闭写端,父进程读数据 26 int n = read(fd[0],buf,sizeof(buf)); 27 write(STDOUT_FILENO,buf,n); 28 close (fd[1]); 29 } 30 return 0; 31 }
二、命名管道fifo
可以作用与非血缘关系间进程通信。
通过命令“mkfifo 文件名”或者mkfifo函数:
int mkfifo(const char *pathname, mode_t mode); 成功:0; 失败:-1 ;
读文件的一方:
1 #include<sys/stat.h> 2 #include<fcntl.h> 3 #include<string.h> 4 #include<unistd.h> 5 int main(int argc,char *argv[]){ 6 int fd,len; 7 char *buf[1024]; 8 //提前通过命令创建fifo文件,将文件名通过参数传递进去 9 fd=open(argv[1],O_RDONLY); 10 if(fd==-1){ 11 perror("open is error"); 12 exit(1); 13 } 14 while(1){ 15 len = read(fd,buf,sizeof(buf)); 16 write(STDOUT_FILENO,buf,len); 17 sleep(3); 18 } 19 close(fd); 20 return 0; 21 22 }
写文件一方:
1 #include<stdlib.h> 2 #include<fcntl.h> 3 #include<string.h> 4 #include<sys/stat.h> 5 int main(int argc,char *argv[]){ 6 int fd; 7 int i=1; 8 char buf[1024]; 9 fd = open(argv[1],O_WRONLY); 10 if(fd==-1){ 11 perror("open is error"); 12 exit(1); 13 } 14 while(1){ 15 sprintf(buf,"hello fifo%d\n",i++); 16 write(fd,buf,strlen(buf)); 17 sleep(1); 18 } 19 close(fd); 20 return 0; 21 }
三、存储映射
将磁盘中一个文件映射到内存,于是当从内存缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。可以在不使用系统调用的情况下使用指针操作内存。使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过 mmap 函数来实现。既可以完成血缘关系进程通信,也可以完成非血缘关系进程通信。
void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
返回:成功:返回创建的映射区首地址;失败:MAP_FAILED 宏
参数:
addr: 建立映射区的首地址,由 Linux 内核指定。使用时,直接传递 NULL
length: 欲创建映射区的大小
prot: 映射区权限 PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags: 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
MAP_SHARED: 会将映射区所做的操作反映到物理设备(磁盘)上。
MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。
fd: 用来建立映射区的文件描述符
offset: 映射文件的偏移(4k 的整数倍)
int munmap(void *add,size_t len); 释放映射区
两个无血缘关系的进程通信;
写进程代码:
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<unistd.h> 4 #include<string.h> 5 #include<fcntl.h> 6 #include<sys/mman.h> 7 typedef struct student{ 8 int num; 9 char name[255]; 10 int age; 11 }stu; 12 int main(){ 13 stu s={1,"LiMing",20}; 14 int fd,ret; 15 stu *p; 16 //给读写权限 17 fd=open("test",O_CREAT|O_TRUNC|O_RDWR,0664); 18 if(fd==-1){ 19 perror("open error\n"); 20 exit(1); 21 } 22 //扩展文件大小 23 ftruncate(fd,sizeof(s)); 24 p=mmap(NULL,sizeof(s),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); 25 if(p==MAP_FAILED){ 26 perror("mmap error\n"); 27 exit(1); 28 } 29 close(fd); 30 while(1){ 31 //可以操作指针 32 memcpy(p,&s,sizeof(s)); 33 s.num++; 34 sleep(1); 35 } 36 //关闭映射区 37 ret=munmap(p,sizeof(s)); 38 if(ret==-1){ 39 perror("munmap error\n"); 40 exit(1); 41 } 42 return 0; 43 44 }
读进程代码:
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<unistd.h> 4 #include<string.h> 5 #include<fcntl.h> 6 #include<sys/mman.h> 7 typedef struct student{ 8 int num; 9 char name[255]; 10 int age; 11 }stu; 12 int main(){ 13 int fd; 14 stu s; 15 stu *p; 16 //指定读权限 17 fd=open("test",O_RDONLY); 18 if(fd==-1){ 19 perror("open error\n"); 20 exit(1); 21 } 22 p=mmap(NULL,sizeof(s),PROT_READ,MAP_SHARED,fd,0); 23 //使用完毕后关闭文件描述符 24 close(fd); 25 if(p==MAP_FAILED){ 26 perror("mmap error\n"); 27 } 28 while(1){ 29 printf("id=%d,name=%s,age=%d\n",p->num,p->name,p->age); 30 sleep(1); 31 } 32 munmap(p,sizeof(s)); 33 34 return 0; 35 }
从输出的结果来看,当写端的速度慢,读端的速度快时,发现映射区中的数据可重复读取,这与管道不一样。
父子进程通信,先创建映射区,后fork()
1 #include<stdio.h> 2 #include<string.h> 3 #include<sys/wait.h> 4 #include<stdlib.h> 5 #include<unistd.h> 6 #include<fcntl.h> 7 #include<sys/types.h> 8 #include<sys/mman.h> 9 int main(){ 10 pid_t pid; 11 char *p; 12 int fd; 13 fd=open("text",O_RDWR|O_CREAT|O_TRUNC,0644); 14 ftruncate(fd,20); 15 int len = lseek(fd,0,SEEK_END); 16 p=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); 17 if(p==MAP_FAILED){ 18 perror("mmap error\n"); 19 exit(1); 20 } 21 close(fd); 22 pid=fork(); 23 if(pid==0){ 24 strcpy(p,"hello mmap\n"); 25 }else if(pid>0){ 26 sleep(1); 27 printf("-----------%s\n",p); 28 wait(NULL); 29 int ret = munmap(p,len); 30 if(ret==-1){ 31 perror("munmap error\n"); 32 exit(1); 33 } 34 } 35 36 return 0; 37 }
四、信号
进程A 给 进程B 发送信号,B进程收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。信号是软件层面上实现的中断,早期常被称 为“软中断”。
1、特点
简单、不能携带大量信息、满足条件才能发送
一旦信号产生,无论程序执行到什么位置,必须立即停止,处理信号。处理结束后,再继续每个进程收到的所以信号,都是由内核负责发送,内核处理。
2、信号屏蔽字和未决信号集
阻塞信号集:本质位图。将某些信号加入集合,对他们设置屏蔽,当屏蔽某一信号后再收到该信号,该信号的处理将退后,在解除屏蔽前一直处于未决态
未决信号集:位图,用于记录信号的处理状态。表示信号已经产生,但尚未被处理
1、信号产生,未决信号集中描述该信号的位立即翻转为1,表示信号处理未决状态当信号被处理后,对应位翻转为0。这一刻非常快。
2、信号产生后由于某些原因(阻塞)不能递达,这类信号为未决信号集。
3、信号的产生
信号可由按键产生:ctrl+c、ctrl+z、ctrl+\;
系统调用产生: kill raise abort;
软件条件产生 :定时器 alarm、setitimer
硬件异常产生:非法访问内存(段错误)、除0、内存对齐出错(总线错误)
命令产生 :kill命令
1) int kill(pid_t pid, int sig); 成功:0;失败:-1 ,设置errno;
参数1:指定进程id;
参数2:信号名
2)设置定时器(闹钟)。在指定 seconds 后,内核会给当前进程发送 14)SIGALRM 信号。进程收到该信号,默认动 作终止。
每个进程都有且只有唯一个定时器。
unsigned int alarm(unsigned int seconds); 返回 0 或剩余的秒数;
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); 成功:0;失败:-1,设置 errno
参数:which:指定定时方式
自然定时: ITIMER_REAL 发送siglarm信号
虚拟空间计时(用户空间) ITIMER_VIRTUAL 只计算进程占用处理机的时间;
运行时计时(用户+内核) ITIMER_PROF 计算进程占用处理机和系统调用的时间
old_value:传出时间;
new_value:定时秒数;
struct itimerval{
struct timeval it_interval /*next val*/
struct timeval it_value /*cur val*/
};
struct timerval{
time_t tv_sec; //秒
susecond tv_usec //微秒
};
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<unistd.h> 4 #include<signal.h> 5 #include<sys/time.h> 6 void func(int signo) 7 { 8 printf("hello world\n"); 9 } 10 int main() 11 { 12 struct itimerval it,oldit; 13 //设置信号捕捉 14 signal(SIGALRM,func); 15 //初始化 16 //首次2秒以后发送信号 17 it.it_value.tv_sec= 2; /*秒 */ 18 it.it_value.tv_usec=0; //微秒 19 //以后每隔5秒发送信号 20 it.it_interval.tv_sec = 5; 21 it.it_interval.tv_usec = 0; 22 if(setitimer(ITIMER_REAL,&it,&oldit)==-1) 23 { 24 perror("setitmer error\n"); 25 return -1; 26 } 27 while(1); 28 29 }
4、信号集操作函数
PCB中存放的有阻塞信号集和未决信号集,未决信号集不提供给用户来操作它的方式和方法,但是我们可以利用操作阻塞信号集来影响未决信号集。未决信号集和阻塞信号集默认都是0,当有一个信号产生,未决信号集会有一个位由0翻转成1,如果没有阻塞,内核会很快处理掉这个信号,由1翻转成0。如果我们不想某个信号被内核处理,可以设置阻塞信号集里对应的位,由0翻转成1。要想完成这一系列操作需要用到一些函数:
sigset_t set; //自定义一个信号集,因为操作系统也不允许直接操控阻塞信号集,利用这个自定义信号集来影响阻塞信号集
int sigemptyset(sigset_t *set); //将信号集全部清0
int sigfillset(sigset_t *set); //将信号集全部设置为1
int sigaddset(sigset_t *set,int signum); //将某个信号加入到信号集中
int sigdelset(sigset_t *set,int signum); //将某个 信号移除信号集
int sigismember(const sigset_t *set,int signum); //判断某个信息是否在信号集中,如果在集合返回1,不在0
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 成功:0;失败:-1,设置 errno
参数:
set:传入参数,是一个位图,set 中哪位置 1,就表示当前进程屏蔽哪个信号。
oldset:传出参数,保存旧的信号屏蔽集。
how 参数取值: 假设当前的信号屏蔽字为 mask
1. SIG_BLOCK: 当 how 设置为此值,set 表示需要屏蔽的信号。相当于 mask = mask|set
2. SIG_UNBLOCK: 当 how 设置为此,set 表示需要解除屏蔽的信号。相当于 mask = mask & ~set
3. SIG_SETMASK: 当 how 设置为此,set 表示用于替代原始屏蔽及的新屏蔽集。相当于 mask = set
int sigpending(sigset_t *set); set 传出参数 //读取进程的未决信号集
例子:对一个信号屏蔽,并打印进程的未决信号集
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<unistd.h> 4 #include<signal.h> 5 void print_pending(sigset_t *pedset){ 6 int i; 7 for(i=1;i<32;i++){ 8 //判断某个信号是否在未决信号集中 9 if(sigismember(pedset,i)){ 10 putchar('1'); 11 }else 12 putchar('0'); 13 } 14 printf("\n"); 15 } 16 int main(){ 17 sigset_t set,oldset,pedset; 18 int ret; 19 //清空自定义信号集 20 sigemptyset(&set); 21 //将信号添加到自定义信号集 22 ret=sigaddset(&set,SIGINT); 23 if(ret==-1){ 24 perror("sigaddset erroe"); 25 exit(1); 26 } 27 //屏蔽信号 28 ret= sigprocmask(SIG_BLOCK,&set,&oldset); 29 if(ret==-1){ 30 perror("sigprocmask error"); 31 exit(1); 32 } 33 while(1){ 34 //获取未决信号集 35 ret=sigpending(&pedset); 36 if(ret==-1){ 37 perror("sigpending error"); 38 exit(1); 39 } 40 //打印 41 print_pending(&pedset); 42 sleep(1); 43 } 44 return 0; 45 }
打印结果:
1 0000000000000000000000000000000 2 ^C 3 0100000000000000000000000000000
开始并没有信号产生,未决信号集全0,当按键产生一个信号SIGINT,编号为2,由于对它进行了屏蔽,未决信号集即使对应位翻转成1,内核也不会处理。
5、信号捕捉
对信号的处理有三种,第一执行默认动作,第二,丢弃,第三,捕捉,执行自定义函数。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 成功:0;失败:-1,设置 errno
参数:
act:传入参数,新的处理方式。
oldact:传出参数,旧的处理方式。
struct sigaction {
void (*sa_handler)(int); //指定信号捕捉后的处理函数
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; //调用信号处理函数时,所要屏蔽的信号集合,只在捕捉函数执行期间有用
int sa_flags; //默认为0,表明某一个信号捕捉函数执行期间,此信号自动被屏蔽。如果捕捉到信号后,在执行期间,不希望自动阻塞该信号,可以将sa_flags设为SA_NODEFER
void (*sa_restorer)(void);
};
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<unistd.h> 4 #include<signal.h> 5 void catch_sig(int signo){ 6 printf("catch %d\n",signo); 7 sleep(10); 8 return; 9 10 } 11 int main(){ 12 struct sigaction act,oldact; 13 act.sa_handler=catch_sig; //捕捉函数名 14 sigemptyset(&(act.sa_mask)); //清空sa_mask 15 sigaddset(&act.sa_mask,SIGQUIT); //在捕捉函数执行期间,ctrl +\信号被忽略 16 //捕捉函数执行期间,被捕捉的信号被屏蔽 17 act.sa_flags=0; 18 //注册函数捕捉函数 19 sigaction(SIGINT,&act,&oldact); 20 while(1); 21 return 0; 22 }
打印结果分析
1 ^Ccatch 2 2 ^\^\^\^\^C^C^C^C^C^C^C^Ccatch 2
在捕捉函数执行期间,十秒内,SIGINT信号和SIGQUIT信号被屏蔽,待捕捉函数执行完毕再对信号进行处理。注意,阻塞的常规信号不支持排队,产生多次只记录一次,内核只处理一次。
6、一个特殊的信号——SIGCHLD
在父进程回收子进程的问题中,有一个复杂情况:如果父进程调用exec函数族,除非函数调用失败,否则不会调用后面的wait函数。如果在exec函数族之前调用wait函数回收子进程,因为wait函数阻塞等待子进程结束,在子进程结束之后才能执行exec函数,就算使用waitpid函数回收,设置非阻塞,也要忙轮训。SIGCHLD信号可以很好的解决这个问题。
子进程在终止时,在收到信号SIGSTOP停止然后再收到SIGCONT信号继续时都会发送SIGCHLD信号,而父进程在收到该函数时默认忽略该信号,所以要注册函数捕捉函数。
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<unistd.h> 4 #include<sys/wait.h> 5 #include<signal.h> 6 void catch_child(int signo){ 7 int status; 8 pid_t wpid; 9 //如果此时此刻有多个子进程死亡,忙轮询可以在一次调用中,回收多个子进程 10 while((wpid=waitpid(-1,&status,0))!=-1){ 11 //得到子进程返回值 12 if(WIFEXITED(status)){ 13 printf("-----catch child ---pid =%d ,return %d\n", 14 wpid,WEXITSTATUS(status)); 15 } 16 } 17 return; 18 } 19 int main(){ 20 pid_t pid; 21 //阻塞SIGCHLD,防止在注册捕捉函数之前,就有子进程死亡发送信号 22 sigset_t set,oldset; 23 sigemptyset(&set); 24 sigaddset(&set,SIGCHLD); 25 sigprocmask(SIG_BLOCK,&set,&oldset); 26 int i; 27 for(i=0;i<5;i++){ 28 pid=fork(); 29 if(pid==0) 30 break; 31 } 32 if(5==i){ 33 //注册信号捕捉函数 34 struct sigaction act,oldact; 35 act.sa_handler=catch_child; 36 sigemptyset(&act.sa_mask); 37 act.sa_flags=0; 38 sigaction(SIGCHLD,&act,&oldact); 39 //解除阻塞,可以处理子进程发来的信号 40 sigprocmask(SIG_UNBLOCK,&set,&oldset); 41 printf("i am parent pid=%d\n",getpid()); 42 while(1); 43 } 44 else{ 45 printf("i am %d child pid =%d\n",i+1,getpid()); 46 return i+1; 47 } 48 return 0; 49 }
五、本地套接字
参考网络套接字实现
服务器端代码:
1 #define SERSOCKET "ser.socket" 2 int main() 3 { 4 int lfd,cfd,len,i,n; 5 char buf[BUFSIZ]; 6 //本地套接字的地址结构 7 struct sockaddr_un ser_addr,cli_addr; 8 //清空 9 bzero(&ser_addr,sizeof(ser_addr)); 10 11 ser_addr.sun_family=AF_UNIX; 12 strcpy(ser_addr.sun_path,SERSOCKET); 13 14 lfd = socket(AF_UNIX,SOCK_STREAM,0); 15 len = offsetof(struct sockaddr_un,sun_path)+strlen(ser_addr.sun_path); 16 unlink(SERSOCKET); 17 //bind()调用成功会创建出来一个socket文件,用于两端进行通信 18 bind(lfd,(struct sockaddr*)&ser_addr,len); 19 20 Listen(lfd,20); 21 22 printf("accpct...\n"); 23 24 while(1) 25 { 26 len=sizeof(cli_addr); 27 cfd=accept(lfd,(struct sockaddr*)&cli_addr,(socklen_t*)&len); 28 while((n=read(cfd,buf,sizeof(buf)))!=NULL) 29 { 30 for(i=0;i<n;i++) 31 { 32 buf[i]=toupper(buf[i]); 33 } 34 write(cfd,buf,n); 35 } 36 close(cfd); 37 } 38 close(lfd); 39 }
客户端代码:
1 #define SERSOCKET "ser.socket" 2 #define CLISOCKET "cli.socket" 3 int main() 4 { 5 int fd,len,n; 6 char buf[BUFSIZ]; 7 struct sockaddr_un ser_addr,cli_addr; 8 bzero(&cli_addr,sizeof(cli_addr)); 9 fd=socket(AF_UNIX,SOCK_STREAM,0); 10 cli_addr.sun_family=AF_UNIX; 11 strcpy(cli_addr.sun_path,CLISOCKET); 12 13 len = offsetof(struct sockaddr_un,sun_path)+strlen(cli_addr.sun_path); 14 //客户端也需要绑定地址结构 15 unlink(CLISOCKET); 16 bind(fd,(struct sockaddr*)&cli_addr,len); 17 18 bzero(&ser_addr,sizeof(ser_addr)); 19 ser_addr.sun_family = AF_UNIX; 20 strcpy(ser_addr.sun_path,SERSOCKET); 21 22 len = offsetof(struct sockaddr_un,sun_path)+strlen(ser_addr.sun_path); 23 connect(fd,(struct sockaddr*)&ser_addr,len); 24 25 while(fgets(buf,sizeof(buf),stdin)!=NULL) 26 { 27 write(fd,buf,strlen(buf)); 28 n = read(fd,buf,sizeof(buf)); 29 write(STDOUT_FILENO,buf,n); 30 } 31 close(fd); 32 33 }