常见的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  }