进程间通讯

  进程间通讯

  在进程控制开发中,读者已经学会了如何建立进程以及如何对进程进行基本的控制,这些都只是停留在父子进程之间的控制,本章将样学习不一样的进程间进行通讯的方法。经过本章学习,读者将会掌握如下内容:程序员

  • 掌握Linux中管道的基本概念
  • 掌握Linux中管道的建立
  • 掌握Linux中管道的读写
  • 掌握Linux中有名管道的建立读写方法
  • 掌握Linux中消息队列的处理
  • 掌握Linux中共享内存的处理

 

8.1 Linux下进程间通讯概述

  在上一章中,读者已经知道了进程是一个程序的依稀执行的过程。这里所说的进程通常是指运行在用户态的进程,而因为处于用户态的不一样进程之间是彼此隔离的,就像处于不一样城市的人们,它们必须经过某种方式来提供通讯,例如人们如今普遍使用的手机等方式。本章就是讲述如何创建这些不一样的通话方式,就像人们有不少种通讯方式同样。shell

  Linux下的进程通讯手段基本上是从UNIX平台上的进程通讯手段继承而来的。而对UNIX发展作出重要贡献的两大主力AT&T的贝尔实验室即BSD在进程间的通讯方面的侧重点有所不一样。前者是对UNIX早期的进程间通讯手段进行了系统的改进和扩充,造成了“systemV PIC”,其通讯进程主要局限在单个计算机内;后者则跳过了该限制,造成了基于套接字(socket)的进程间通讯机制。而Linux则把二者的优点都继承了下来。如图:数组

  • UNIX进程间通讯(IPC)方式包括管道、FIFO、信号。
  • System V进程间通讯(IPC)包括System V 消息队列、System V 信号灯、System V 共享内存区。
  • Posix进程间通讯(IPC)包括Posix消息队列、Posix信号灯、Posix共享内存区。

 

  如今在Linux中使用较多的进程间通讯方式主要有一下几种。  app

  (1) 管道(pipe)及有名管道(named pipe):管道可用于具备亲缘关系进程间的通讯;有名管道,除具备管道所具备的的功能外,它还容许无亲缘关系进程间的通讯。ssh

  (2) 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通讯方式,用于通知接受进程有某个事件发生,一个进程收到一个信号与处理器收到一个中断请求的效果上能够说是同样的。异步

  (3) 消息队列:消息队列是消息的连接表,包括Posix消息队列、SystemV消息队列。它克服了前两种通讯方式中信息量有限的缺点,具备写权限的进程能够向消息队列中按照必定的规则添加新消息:对消息队列有读权限的进程则能够从消息队列中读取消息。socket

  (4) 共享内存:能够说这是最有用的进程间通讯方式。它使得多个进程能够访问同一块内存空间,不一样进程能够及时看到对方进程中对共享内存中数据的更新。这种通讯方式须要依靠某种同步机制,如互斥锁和信号量等。ide

  (5) 信号量:主要做为进程间以及同一进程不一样线程之间的同步手段。函数

  (6) 套接字(Socket):这是一种更为通常的进程间通讯机制,它可用于不一样机器之间的进程间通讯,应用很是普遍。post

 

8.2 管道通讯

  8.2.1 管道概述

  细心的读者可能会注意到本书在第2张中介绍“ps”的命令时提到过管道,当时指出了管道是Linux中很重要的一种通讯方式,它是把一个程序的输出直接链接到另外一个程序的输入,这里以第2章中的 ps -ef|grep ntp 为例,描述管道的通讯过程,如图:

 

   管道是Linux中进程间通讯的一种方式。这里所说的管道主要指无名管道,它具备以下特色:

  • 它只能用于具备亲缘关系的进程之间的通讯(也就是父子进程或者兄弟进程之间)。
  • 它是一个半双工的通讯模式,具备固定的读端和写端。
  • 管道也能够当作是一种特殊的文件,对于它的读写也可使用普通的read、write等函数。可是它不是普通的文件,并不属于其余任何文件系统,而且只存在于内存中。

  8.2.2 管道建立与关闭

   1. 管道建立与关闭说明

   管道是基于文件描述符的通讯方式,当一个管道创建时,它会建立两个文件描述符fds[0]和fds[1],其中fds[0]固定于读管道,而fds[1]固定于写管道,这就构成了一个半双工的通道。

 

  管道关闭时只须要将两个文件描述符关闭便可,可以使用普通的close函数逐个关闭各个文件描述符。

  注意:一个管道共享了多对文件描述符时,若将其中一对读写文件描述符都删除,则该管道就时效。

  2. 管道建立函数

  建立管道能够经过调用pipe来实现

所需头文件 #include<unistd.h>
函数原型 int pipe(int fd[2])
函数传入值 fd[2]:管道的两个文件描述符,以后就能够直接操做这两个文件描述符
函数返回值 成功:0
出错:-1

 

   3. 管道建立实例

  建立管道很是简单,只须要调用函数pipe便可:

#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
    int pipe_fd[2];

    /* 建立一个无名管道 */
    if(pipe(pipe_fd)<0){
        printf("pipe create error\n");
        return -1;
    }else
        printf("pipe create success\n");

    /* 关闭管道描述符 */
    close(pipe_fd[0]);
    close(pipe_fd[1]);
}
pipe.c

  程序运行后先成功建立一个无名管道,以后再将其关闭。

 

  8.2.3 管道读写

  1. 管道读写说明

  用pipe函数建立的管道两端处于一个进程中,因为管道是主要用在不一样进程间通讯的,所以这在实际应用中没有太大意义。实际上,一般先是建立一个管道,在经过fork()函数建立一子进程,该子进程就会继承父进程锁建立的管道,这时,父子进程管道的文件描述符对应关系如图:

  这时的关系看似很是复杂,实际上却已经给不一样进程之间的读写创造了很好的条件。这时,父子进程分别拥有本身的读写的通道,为了实现父子进程之间的读写,只须要把无关的读端或写端的文件描述符关闭便可。例如把父进程的写端fd[1]和子进程的读端fd[0]关闭。这时,父子进程之间就创建起了一条“子进程写入父进程读”的通道。

  一样,也能够关闭父进程fd[0]和子进程fd[1],这样就能够创建一条“父进程写子进程读”的通道。另外,父进程还能够建立多个子进程,各个子进程都继承了相应的fd[0]和fd[1],这时,只须要关闭相应端口就能够创建起各个子进程之间的通讯。

  想想:为何无名管道只能创建具备亲缘关系的进程之间?

  

  2.管道读写实例

  在本例中,首先建立管道,以后父进程使用fork函数建立子进程,以后经过关闭父进程的读描述符和子进程的写描述符,创建起它们之间的管道通讯。

#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>

int main()
{
    int pipe_fd[2];
    pid_t pid;
    char buf_r[100];
    char *p_wbuf;
    int r_num;
    memset(buf_r,0,sizeof(buf_r));

    /* 建立管道 */
    if((pipe(pipe_fd))<0){
        printf("pipe create error\n");
        return -1;
    }

    /* 建立一个子进程 */
    if((pid=fork())==0){
        printf("\n");
        /* 关闭子进程写描述符,并经过使父进程暂停2秒确保父进程以关闭相应的读描述符 */
        close(pipe_fd[1]);
        sleep(2);

        /* 子进程读取管道内容 */
        if((r_num=read(pipe_fd[0],buf_r,100))>0){
            printf("%d numbers read from the pipe is %s\n",r_num,buf_r);
        }

        /* 关闭子进程读描述符 */
        close(pipe_fd[0]);
        exit(0);
    }else if(pid>0){
        /* 关闭父进程读描述符,并分两次向管道中写入 Hello Pipe */
        close(pipe_fd[0]);
        if(write(pipe_fd[1],"Hello",5)!=-1)
            printf("parent write1 success!\n");
        if(write(pipe_fd[1]," Pipe",5)!=-1)
            printf("parent write2 success!\n");
        /* 关闭符进程写描述符 */
        close(pipe_fd[1]);
        sleep(3);
        /* 收集子进程退出信息 */
        waitpid(pid,NULL,0);
        exit(0);
    }
    
}
pipe_rw.c

  运行结果:

parent write1 success!
parent write2 success!

10 numbers read from the pipe is Hello Pipe
pipe_rw.c

 

  3. 管道读写注意点

  • 只有在管道的读端存在时,向管道中写入数据才有意义。不然,向管道中写入数据的进程将收到内核传来的SIFPIPE信号(一般Broken pipe错误)。
  • 向管道中写入数据时,Linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。若是读进程不读取管道缓冲区中的数据,那么写操做将会一直阻塞。
  • 父子进程在运行时,它们的前后次序并不能保证,所以,在这里为了保证父进程已经关闭了读描述符,可在子进程中调用sleep函数。

  8.2.4 标准流管道

   1. 标准流管道函数说明

  与Linux中文件操做有基于文件流的标准I/O操做同样,管道的操做也支持基于文件流的模式。这种基于文件流的管道主要用来建立一个链接到另外一个进程的管道,这里的“另外一个进程”也就是一个能够执行必定操做的可执行文件,例如,用户执行“cat popen.c”或者本身编写的程序"hello"等。因为这一类操做很常见,所以标准流管道就将一系列的建立过程合并到一个函数popen中完成。它所完成的工做有一下几步:

  • 建立一个管道
  • fork一个子进程
  • 在父子进程中关闭不须要的文件描述符
  • 执行exec函数族调用
  • 执行函数中所指定的命令。

  这个函数的使用能够大大减小代码的编写量,但同时也有一些不利之处,例如,它没有前面管道建立的函数灵活多样,而且用popen建立的管道必须使用标准I/O函数进行操做,但不能使用前面的read、write一类不带缓冲的I/O函数。

  与之相对应,关闭用popen建立的流管道必须使用函数pclose来关闭该管道流。该函数关闭标准I/O流,并等待命令执行结束。

  2.函数格式

  popen函数格式:

所需头文件 #include<stdio.h>
函数原型 FILE *popen(const char *command,const char *type)
函数传入值 command:指向的是一个以null结束符结尾的字符串,这个字符串包含一个shell命令,并被送到/bin/sh以-c参数执行,即由shell来执行
type

"r":文件指针链接到command的标准输出,即该命令的结果产生输出

"w":文件指针链接到command的标准输入,即该命令的结果产生的输入

函数返回值 成功:文件流指针
出错:-1

 

  pclose函数格式

所需头文件 #include<stdio.h>
函数原型 int pclose(FILE *stream)
函数传入值 stream:要关闭的文件流
函数返回值 成功:返回popen中执行命令的终止状态
  出错:-1

  

  3. 函数使用实例

  在该实例中,使用popen来执行“ps -ef”命令。能够看出,popen函数的使用可以使程序变得短小精悍。

#include <stdio.h>
#include <stdlib.h>


#define BUFSIZE 1000

int main()
{
    FILE *fp;
    char *cmd="ps -ef";
    char buf[BUFSIZE];

    /* 调用popen函数执行相应的命令 */
    if((fp=popen(cmd,"r"))==NULL)
        perror("popen");
    while((fgets(buf,BUFSIZE,fp))!=NULL)
        printf("%s",buf);
    pclose(fp);
    exit(0);
}
popen.c

  运行结果:

UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 10:23 ?        00:00:02 /sbin/init splash
root          2      0  0 10:23 ?        00:00:00 [kthreadd]
root          4      2  0 10:23 ?        00:00:00 [kworker/0:0H]
root          6      2  0 10:23 ?        00:00:00 [mm_percpu_wq]
root          7      2  0 10:23 ?        00:00:00 [ksoftirqd/0]
root          8      2  0 10:23 ?        00:00:01 [rcu_sched]
root          9      2  0 10:23 ?        00:00:00 [rcu_bh]
root         10      2  0 10:23 ?        00:00:00 [migration/0]
root         11      2  0 10:23 ?        00:00:00 [watchdog/0]
root         12      2  0 10:23 ?        00:00:00 [cpuhp/0]
root         13      2  0 10:23 ?        00:00:00 [kdevtmpfs]
......
root       5036      2  0 16:11 ?        00:00:00 [kworker/0:0]
root       5075      2  0 16:22 ?        00:00:00 [kworker/0:2]
root       5190    898  0 16:25 ?        00:00:00 sshd: abc [priv]
abc        5216   5190  0 16:25 ?        00:00:00 sshd: abc@notty
abc        5217   5216  0 16:25 ?        00:00:00 /usr/lib/openssh/sftp-server
abc        5230   3621  0 16:26 pts/4    00:00:00 ./popen
abc        5231   5230  0 16:26 pts/4    00:00:00 sh -c ps -ef
abc        5232   5231  0 16:26 pts/4    00:00:00 ps -ef
popen.c

 

  8.2.5 FIFO

  1. 有名管道说明

  前面介绍的管道是无名管道,它只能用于具备亲缘关系的进程之间,这就大大地限制了管道的使用。有名管道的出现突破了这种限制,它可使互不相关的两个进程实现彼此通讯。该管道能够经过路径名来指出,而且在文件系统中是可见的。在创建了管道以后,两个进程就能够把它当作普通文件同样进行读写操做,使用很是方便。不过值得注意的事,FIFO是严格遵循先进先出规则的,对管道及FIFO的读老是从开始处返回数据,对它们的写则把数据添加到末尾,它们不支持如lseek()等文件定位操做。

  有名管道的建立可使用函数mkfifo(),该函数相似文件中的open()操做,能够指定管道的路径和打开的模式。

  小知识:用户还能够在命令行使用 “mknod 管道名 p” 来建立有名管道。

  在建立管道成功以后,就可使用open、read、write这些函数了。与普通文件的开发设置同样,对于为读而打开的管道可在open中设置O_RDONLY,对于为写而打开的管道可在open中设置O_WRONLY,在这里与普通文件不一样的是阻塞问题。因为普通文件的读写时不会出现阻塞问题,而在管道的读写中却有阻塞的可能,这里的非阻塞标志能够在open函数中设定为O_NONBLOCK。下面分别对阻塞打开和非阻塞打开的读写进行必定的讨论。

  对于读进程

  • 若该管道是阻塞打开,且当前FIFO内没有数据,则对读进程而言将一直阻塞直到有数据写入。
  • 若该管道是非阻塞打开,则不论FIFO内是否有数据,读进程都会当即执行读操做。

  对于写进程

  • 若该管道是阻塞打开,则写进程而言将一直阻塞直到有读进程读出数据
  • 若该管道是非阻塞打开,则当前FIFO内没有读操做,写进程都会当即执行读操做。

  2. mkfifo函数格式

  mkfifo函数的语法要点

所需头文件 #include<sys/types.h>
#include<sys/state.h>
函数原型 int mkfifo(const char *filename,mode_t mode)
函数传入值 filename:要建立的管道
mode O_RDONLY:读管道
O_ERONLY:写管道
O_REWR:读写管道
O_NONBLOCK:非阻塞
O_CREAT:若是该文件不存在,那么就建立一个新的文件,并用第三个参数为其设置权限。
O_EXCL:若是使用O_CREAT时文件存在,那么可返回错误消息。这一参数可测试文件是否存在
函数返回值 成功:0
出错:-1

  

  FIFO相关的出错信息

EACCESS 参数filename所指定的目录路径无可执行的权限
EEXIST 参数filename所指定的文件已存在
ENAMETOOLONG 参数filename的路径名太长
ENOENT 参数filename包含的目录不存在
ENOSPC 文件系统的剩余空间不足
ENOTDIR 参数filename路径中的目录存在到却非真正的目录
EROFS 参数filename指定的文件存在于只读文件系统内

  

  3.使用实例

  下面的实例包含了两个程序,一个用于读管道,另外一个用于写管道。其中在写管道的程序里建立管道,而且做为main函数里的参数由用户输入要写入的内容。读管道读出了用户写入管道的内容,这两个函数用的是非阻塞读写管道。

 

#include <string.h>
#include <unistd.h>


#define FIFO_SERVER "/tmp/myfifo"

int main(int argc,char *argv[])
{
    int fd;
    char w_buf[100];
    int nwrite;
    if(fd==-1)
        if(errno==ENXIO)
            printf("open error;no reading process\n");

    /* 打开FIFO管道,并设置非阻塞标志 */
    fd = open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0);
    if(argc==1)
        printf("Please send something\n");
    strcpy(w_buf,argv[1]);
    /* 向管道中写入字符串 */
    if((nwrite=write(fd,w_buf,100))==-1){
        if(errno==EAGAIN)
            printf("The FIFO has not been read yet.Please try later\n");
    }else
        printf("write %s to the FIFO\n",w_buf);
}
fifo_write.c

 

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>

#define FIFO "/tmp/myfifo"

int main(int argc,char *argv[])
{
    char buf_r[100];
    int fd;
    int nread;

    /* 建立有名管道,并设置相应的权限 */
    if((mkfifo(FIFO,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))
        printf("cannot create fifoserver\n");
    printf("Preparing for reading bytes...\n");
    memset(buf_r,0,sizeof(buf_r));

    /* 打开有名管道,并设置非阻塞标志 */
    fd = open(FIFO,O_RDONLY|O_NONBLOCK,0);
    if(fd==-1){
        perror("open");
        exit(1);
    }
    while(1){
        memset(buf_r,0,sizeof(buf_r));
        if((nread=read(fd,buf_r,100))==-1){
            if(errno==EAGAIN)
                printf("no data yet\n");
        }
        printf("read %s from FIFO\n",buf_r);
        sleep(1);
    }
    pause();
    unlink(FIFO);
}
fifo_read.c

  为了能更好地观察运行结果,须要把这两个程序分别在两个终端里运行,在这里首先启动读管道程序。因为这是非阻塞管道,所以在创建管道以后程序就开始循环从管道里读出内容。在启动了写管道程序后,读进程可以从读管道里读出用户的输入内容,程序运行结果以下:

abc@pc:~/c/app$ sudo ./fifo_read
[sudo] password for abc:
Preparing for reading bytes...
read  from FIFO
read  from FIFO
read  from FIFO
read  from FIFO
read 123456 from FIFO
read  from FIFO
read  from FIFO
read  from FIFO
read  from FIFO
read  from FIFO
read Hello from FIFO
read  from FIFO
read  from FIFO
read  from FIFO
read  from FIFO
fifo_read.c

 

abc@pc:~/c/app$ ./fifo_write 123456
write 123456 to the FIFO
abc@pc:~/c/app$ ./fifo_write Hello
write Hello to the FIFO
fifo_write.c

 

8.3 信号通讯

  8.3.1 信号概述

   信号是UNIX中所使用的进程间通讯的一种最古老的方法。它是在软件层次上对中断机制的一种模拟,是一种异步通讯方式。信号能够直接进行用户空间进程和内核进程之间的交互,内核进程也能够利用它来通知用户空间进程发生了哪些系统时间。它能够在任什么时候候发给某一进程,而无需知道该进程的状态。若是该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它为止;若是一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

  细心的读者是否还记得,在第2章kill命令中曾讲解到“-l”选项,这个选项能够列出该系统所支持的全部信号列表。在笔者的系统中,信号值在32以前的则有不一样的名称,而信号值在32以后的都是用“SIGRTMIN”或“SIGRTMAX”开头的,这就是两种典型的信号。前者是从UNIX系统中继承下来的信号,为不可靠信号(也成为非实时信号);后者是为了解决前面"不可靠信号"的问题而进行了更改和扩充的信号,成为“可靠信号”(也称为实时信号)。那么为何以前的信号不可靠呢?这里首先要介绍一下信号的声明周期。

  一个完整的信号声明周期能够分为3个重要阶段,这3个阶段由4个重要事件来刻画的:信号产生、信号在进程中注册、信号在进程中注销、执行信号处理函数,如线图所示:

  相邻两个事件的时间间隔构成信号声明周期的一个阶段。要注意这里的信号处理有多重方式,通常是由内核完成的,固然也能够由用户进程来完成,故在此没有明确画出。

  一个不可靠信号的处理过程是这样的:若是发现该信号已经在进程中注册,那么久忽略该信号。所以,前一个信号还未注销又产生了相同的信号就会产生信号丢失。而当可靠信号发送给一个进程时,无论该信号是否已经在进程中注册,都会被再注册一次,所以信号就不会丢失。全部可靠信号都支持排队,而不可靠信号则都不支持排队。

  注意:这里信号的产生、注册、注销等是指信号的内部实现机制,而不是信号的函数实现。所以,信号注册与否,与本节后面降到的发送信号函数(如kill()等)以及信号安装函数(如signal()等)无关,只与信号值有关。

  用户进程对信号的响应能够由3中方式。

  • 忽略信号,即对信号不作任何处理,可是有两个信号不能忽略,,即SIGKILL即SIGSTOP。
  • 捕捉信号,定义信号处理函数,当信号发生时,执行相应的处理函数。
  • 执行缺省操做,在Linux对每种信号都规定了默认操做。

  Linux中大多数信号是提供给内核的,下表列出Linux中最为常见信号的函数及其默认操做。

信号名 含义 默认操做
SIGHUP 该信号在用户端连接(正常或非正常)结束时发出,一般是在终端的控制进程结束时,通知同一会话内的各个做业与控制终端再也不关联。 终止
SIGINT 该信号在用户键入INTR字符时发出,终端驱动程序发送此信号并送到前台进程中的每个进程。 终止
SIGQUIT 该信号与SIGINT相似,但由QUIT字符(一般是Ctrl-\)来控制。 终止
SIGILL 该信号在一个进程企图执行一条非法指令时(可执行文件自己出现错误,或者试图直线数据段、堆栈溢出时)发出。 终止
SIGFPE 该信号在发生致命的算术运算错误是发出。这里不只包括浮点运算错误,还包括溢出及除数为0等其余全部的算术的错误。 终止
SIGKILL 该信号用来当即结束程序的运行,而且不能被阻塞、处理和忽略。 终止
SIGALRM 该信号当一个定时器到时的时候发出 终止
SIGSTOP 该信号用于暂停一个进程,且不能被阻塞、处理货忽略 暂停进程
SIGTSTP 该信号用于交互中止进程,用户可键入SUSP字符时(一般是Ctrl+Z)发出这个信号。 中止进程
SIGCHLD 子进程改变状态是,父进程会受到这个信号 忽略
SIGABORT    

  

  8.3.2 信号发送与捕捉

   发送信号的函数主要有kill()、raise()、alarm()以及pause(),下面就一次对其进行介绍。

  1. kill()和raise()

  (1) 函数说明

  kill函数同读者熟知的kill系统命令同样,能够发送信号给进程或进程组(实际上,kill系统命令只是kill函数的一个用户接口)。这里要注意的事,它不只能够停止进程(实际上发送SIGKILL信号),也能够向进程发送其余信号。

  与kill函数所不一样的是,raise函数容许进程间向自身发送信号。

  (2) 函数格式

  kill

所需头文件 #include<signal.h>
#include<sys/types.h>
函数原型 int kill(pid_t pid,int sig)
函数传入值 pid 正数:要发送信号的进程号
0:信号被发送到全部和pid进程在同一个进程组的进程
-1:信号发送给全部的进程表中的进程(除了进程号最大的进程外)
sig 信号
函数返回值 成功 0
出错 -1

  

  raise

所需头文件 #include<signal.h>
#include<sys/types.h>
函数原型 int raise(int sig)
函数传入值 sig:信号
函数返回值 成功:0
出错:-1

   (3) 函数实例

  下面这个示例首先使用fork建立了一个子进程,接着为了保证子进程不在父进程调用kill以前推出,在子进程中使用raise函数向子进程发送SIGSTOP信号,使子进程暂停。接下来再在父进程中调用kill向子进程发送信号,在该实例中使用的是SIGKILL,读者可使用其余信号进行练习。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main()
{
    pid_t pid;
    int ret;

    /* 建立一子进程 */
    if((pid=fork())<0){
        perror("fork");
        exit(1);
    }
    if(pid==0){
        /* 在子进程中使用raise函数发出SIGSTOP信号 */
        raise(SIGSTOP);
        exit(0);
    }else{
        /* 在父进程中收集子进程发出的信号,并调用kill函数进行相应的操做 */
        printf("pid=%d\n",pid);
        if((waitpid(pid,NULL,WNOHANG))==0){
            if((ret=kill(pid,SIGKILL))==0){
                printf("kill %d\n",pid);
            }else{
                perror("kill");
            }
        }
    }
    
}
kill.c

  运行结果:

abc@pc:~/c/app$ ./kill
pid=20460
kill 20460
abc@pc:~/c/app$ ./kill
pid=20462
kill 20462
kill.c

 

  2. alarm()和pause()

  (1) 函数说明

  alarm也称为闹钟函数,它能够在进程中设置一个定时器,当定时器指定的时间到时,它就向进程饭扫那个SIGALARM信号。要注意的是,一个进程只能有一个闹钟时间,若是在调用alarm以前已经设置过闹钟时间,则任何之前的闹钟时间都被新值所代替。

  pause函数是用于将调用进程挂起直至捕捉到信号位置。这个函数很经常使用,一般能够用于判断信号时候已到。

  (2) 函数格式

  alarm

所需头文件 #inlcude<unistd.h>
函数原型 unsigned int alarm(unsigned int seconds)
函数传入值 seconds:指定秒数
函数返回值 成功:若是调用此alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,不然返回0
出错:-1

   pause

所需头文件 #include<unistd.h>
函数原型 int pause(void)
函数返回值 -1: 而且报error值设为EINTR

 

   (3) 函数实例

  该实例实际上已完成了一个简单的sleep函数的功能,因为SIGALARM默认的系统动做为终止该进程,所以在程序调用pause以后,程序就终止了。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int ret;

    /* 调用alarm定时器函数 */
    ret = alarm(5);
    pause();
    printf("I have waken up. %d\n",ret);
}
alarm.c

  运行结果:

abc@pc:~/c/app$ ./alarm
Alarm clock
alarm.c

 

  8.3.3 信号的处理

   在了解了信号的产生和捕获以后,接线来就要对信号进行具体的操做了。从前面的信号概述中读者也能够看到,特定的信号是与必定的进程相联系的。也就是说,一个进程能够决定在该进程中须要对那些信号进行什么样的处理。例如,一个进程能够选择忽略某些信号而只处理其余一些信号,另外,一个进程还能够选择如何处理信号。总之,这些都是与特定的进程相联系的。所以,首先就要创建其信号与进程之间的对应关系,这就是信号的处理。

  注意:请读者区分信号的注册于信号的处理之间的差异,前者信号是主动方,然后者进程是主动方。信号的注册是在进程选择了特定信号处理以后特定信号的主动行为。

  信号处理的主要方法有两种,一种是使用简单的signal函数,另外一种是使用信号集函数组。下面分别介绍这两种处理方式。

  1. signal()

  (1) 函数说明

  使用signal函数处理时,只需把要处理的信号和处理函数列出便可。它主要是用于前32中非实时信号的处理,不支持信号传递信息,可是因为使用简单、易于理解,所以也收到不少程序员的欢迎。

  (2)函数格式

  signal

所需头文件 #include<signal.h>
函数原型 void (*signal(int signum,void(*handler)(int)))(int)
函数传入值 signum:指定信号
handler SIG_ING:忽略该信号
SIG_DFL:采用系统默认方式处理信号
自定义的信号处理函数指针
函数返回值 成功:之前的信号处理配置
出错:-1

   这里须要对函数原型进行说明。这个函数原型很是复杂。可先用以下typedef进行替换说明:

typedef void sign(int);
sign *signal(int ,handler *);

  可见,首先该函数原型总体指向一个无返回值带一个整形参数的函数指针,也就是信号的原始配置函数。接着该原型有带有两个参数,其中的第二个参数能够是用于自定义的信号处理函数的函数指针。

  (3) 使用实例

  该示例代表了如何使用signal函数捕捉相应信号,并做出给定的处理。这里,my_func就是信号处理的函数指针。读者还能够将其改成SIG_ING或SIG_DFL查看运行结果。

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/* 自定义信号处理函数 */
void my_func(int sign_no)
{
    if(sign_no==SIGINT)
        printf("I have get SIGINT\n");
    else if(sign_no==SIGQUIT)
        printf("I have get SIGQUIT\n");
}

int main()
{
    printf("Waiting for signal SIGINT or SIGQUIT\n");
    /* 发出相应的信号,并跳转到信号处理函数 */
    signal(SIGINT,my_func);
    signal(SIGQUIT,my_func);
    pause();
    exit(0);
}
mysignal.c

  运行结果:

abc@pc:~/c/app$ ./my_signal
Waiting for signal SIGINT or SIGQUIT
^CI have get SIGINT
abc@pc:~/c/app$ ./my_signal
Waiting for signal SIGINT or SIGQUIT
^\I have get SIGQUIT
mysignal.c

 

  2. 信号集函数组

  (1) 函数说明

  使用信号集函数组处理信号时涉及一系列的函数,这些函数按照调用的前后次序可分为如下几大功能模块:建立信号集合登记信号处理器以及检测信号

  其中,建立信号集合主要用于建立用户感兴趣的信号,其函数包括如下几个:

  • sigemptyset:初始化信号集合为空。
  • sigfillset:初始化信号金河为全部信号的集合。
  • sigaddset:将指定信号加入到信号集中去。
  • sigdelset:将指定信号从信号集中删去。
  • sigismember:查询指定信号是否在信号集合中。

  登记信号处理器主要用户决定进程如何处理信号。这里要注意的事,信号集里的信号并非真真能够处理的信号,只要当信号的状态处于非阻塞状态是才真正其做用。所以,首先就要判断出当前阻塞能不能传递给该信号的信号集。这里首先使用sigprocmask函数判断检测或更改信号屏蔽字,而后使用sigaction函数用于改变进程接收到特定信号以后的行为。

  检测信号是信号处理的后续步骤,但不是必须的。因为内核能够在任什么时候刻向某一进程发出信号,所以,若该进程必须保持非中断状态且但愿将这些信号阻塞,这些信号就处于“未决”状态(也就是进程不清楚它的存在)。因此,在但愿保持非中断进程完成相应的任务以后,就应该将这些信号解除阻塞。sigpending函数就容许进程检测“未决”信号,并进一步决定对它们做何处理。

  (2)  函数格式

  首先介绍建立信号集合的函数格式

所需头文件 #inlcude<signal.h>
函数原型 int sigemptyset(sigset_t *set)
int sigfillset(sigset_t *set)
int sigaddset(sigset_t *set,int signum)
int sigdelset(sigset_t *set,int signum)
int sigismember(sigset_t *set,int signum)
函数传入值 set :信号集
signnum:指定信号值
函数返回值 成功:0 (sigismember成功返回1,失败返回0)
出错:-1

   

  sigprocmask

所需头文件 #include<signal.h>
函数原型 int sigprocmask(int how,const sigset_t *set,sigset_t *oset)
函数传入值 how:决定函数的操做方式 SIG_BLOCK:增长一个信号集合到当前进程的阻塞集合中
SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合
SIG_SETMASK:将当前的信号集合设置为信号阻塞集合
set:指定信号集
oset:信号屏蔽字
函数返回值 成功:0(sigismember成功返回1,失败返回0)
出错:-1

   此处,若set是一个非空指针,则参数how表示函数的操做方式;若how为空,则表示忽略此操做。

  

  sigaction

所需头文件 #include<signal.h>
函数原型 int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact)
函数传入值 signum:信号的值,能够为SIGKILL及SIGSTOP外的任何一个特定有效的信号
act:指向结构sigaction的一个实例的指针,指定对特定信号的处理
oldact:保存原来对相对信号的处理
函数返回值 成功:0
出错:-1

   这里要说明的是sigaction函数中第2个和第3个参数用到的sigaction结构。这是一个看似很是复杂的结构,但愿读者可以慢慢阅读此段内容。

  首先给出了sigaction的定义

struct sigaction {
    void (*sa_handler)(int signo);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restore)(void);
}

  sa_handler  是一个函数指针,指定信号关联函数,这里出能够是用户自定义的处理函数外,还能够为SIG_DFL(采用缺省的处理方式)或SIG_IGN(忽略信号)。它的处理函数只有一个参数,即信号值。

  sa_mask 是一个信号集,它能够指定在信号处理程序执行过程当中哪些信号应当被阻塞,在调用信号捕获函数以前,该信号集要加入到信号的信号屏蔽字中。

  sa_flags 中包含了不少标志位,是对信号进行处理的各个选择项。它的常见可选值以下:

选项 含义
SA_NODEFER\SA_NOMASK 当捕捉到此信号时,在执行其信号捕捉函数时,系统不会自动阻塞此信号。
SA_NOCLDSTOP 进程忽略子进程产生的任何SIGSTOP、SIGTSTP、SIGTTIN和SIGTTOU信号。
SA_RESTART 可以让重启的系统调用从新起做用。
SA_ONESHOT\SA_RESETHAND 自定义信号只执行一次,在执行完毕后恢复信号的系统默认动做。

 

  sigpending语法

所需头文件 #include<signal.h>
函数原型 int sigpending(sigset_t *set)
函数传入值 set:要检测的信号集
函数返回值 成功:0
出错:-1

   总之,在处理信号时,通常遵循下图操做流程:

  (3) 使用实例

  该实例首先把SIGQUIT、SIGINT两个信号加入信号集,而后将该信号集设为阻塞状态,并在该状态下使程序暂停5秒。接下来再讲信号集设为非阻塞状态,再对这两个信号分别操做,其中SGIQUIT执行默认操做,而SIGINT执行用户自定义函数的操做。

#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

/* 自定义的信号处理函数 */
void my_func(int signum)
{
    printf("If you want to quit,please try SIQUIT\n");
}

int main()
{
    sigset_t set,pendset;
    struct sigaction action1,action2;

    /* 初始化信号集为空 */
    if(sigemptyset(&set)<0)
        perror("sigemptyset");

    /* 将相应的信号加入信号集 */
    if(sigaddset(&set,SIGQUIT)<0)
        perror("sigaddset");
    if(sigaddset(&set,SIGINT)<0)
        perror("sigaddset");

    /* 设置信号集屏蔽字 */
    if(sigprocmask(SIG_BLOCK,&set,NULL)<0)
        perror("sigprocmask");
    else{
        printf("blocked\n");
        sleep(5);
    }

    if(sigprocmask(SIG_UNBLOCK,&set,NULL)<0)
        perror("sigprocmask");
    else
        printf("unblock\n");

    /* 对应的信号进行循环处理 */
    while(1)
    {
        if(sigismember(&set,SIGINT)){
            sigemptyset(&action1.sa_mask);
            action1.sa_handler = my_func;
            sigaction(SIGINT,&action1,NULL);
        }else if(sigismember(&set,SIGQUIT)){
            sigemptyset(&action2.sa_mask);
            action2.sa_handler = SIG_DFL;
            sigaction(SIGTERM,&action2,NULL);
        }
    }
}
sigaction

  运行结果:

abc@pc:~/c/app$ ./sigaction
blocked
unblock
^CIf you want to quit,please try SIQUIT
^CIf you want to quit,please try SIQUIT
^CIf you want to quit,please try SIQUIT
^CIf you want to quit,please try SIQUIT
^CIf you want to quit,please try SIQUIT
^\Quit (core dumped)
abc@pc:~/c/app$
sigaction

  可见,在信号处于阻塞状态是,所发出的信号对进程不起作。读者等待5秒,在信号解除阻塞状态以后,用户发出的信号才能正常运行。这里的SIGINT已安装用户定义的函数运行。

 

8.4 共享内存

  8.4.1 共享内存概述

  能够说,共享内存是一种最为高效的进程间通讯方式。由于进程能够直接读写内存,不须要任何数据的拷贝。为了在多个进程间交换信息,内核专门留出了一块内存区。这段内存区能够由须要访问的进程将其映射到在即的私有地址空间。所以,进程就能够直接读写这一内存区而不须要进行数据的拷贝,从而大大提升了效率。固然,因为多个进程共享一段内存,所以也须要依靠某种同步机制,如互斥锁和信号量等。其原理示意图:

  

  8.4.2 共享内存实现

   1. 函数说明

  共享内存的实现分为两个步骤,第一步是建立共享内存,这里用到的函数是shmget,也就是从内存中得到一段共享内存区域。第二步映射共享内存,也就是把这段建立的共享内存映射到具体的进程空间去,这里使用的函数是shmat。到这里,就可使用这段共享内存了,也就是可使用不带缓冲的I/O读写命令对其进行操做。除此以外,固然还有撤销映射的操做,其函数为shmdt。这里就主要介绍这3个函数。

  2. 函数格式

  shmget

所需头文件 #include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
函数原型 int shmget(key_t key,int size,int shmflg)
函数传入值 key:IPC_PRIVATE
size: 共享内存区大小
shmflg:同open函数的权位,也能够用八进制表示法
函数返回值 成功:共享内存段标志符
出错:-1

   

  shmat

所需头文件 #inlcude<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
函数原型 char *shmat(int shmid,const *shmaddr,int shmflg)
函数传入值 shmid:要映射的共享内存区标志符
shmaddr:将共享内存映射到指定位置(若为0则表示把该段共享内存映射到调用进程的地址空间)
shmflg SHM_RDONLY:共享内存只读
默认0:共享内存可读写
函数返回值 成功:被映射的段地址
出错:-1

   

  shmdt

所需头文件 #include<sys/types.h>
#include<sys/ipc.h>
#inlcude<sys/shm.h>
函数原型 int shmdt(const void*shmaddr)
函数传入值 shmaddr:被映射的共享内存段地址
函数返回值 成功:0
出错:-1

 

   3. 使用实例

  该实例说明了如何使用基本的共享内存函数,首先是建立一个共享内存区,以后将其映射到被进程中,最后再解除这种映射关系。这里要介绍的一个命令是ipcs,这是用于报告进程间通讯机制状态的命令。它能够查看共享内存、消息队列等各类进程间通讯机制的状况,这里使用了system函数用于调用shell命令“ipcs”。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>

#define BUFSZ 2048

int main()
{
    int shmid;
    char *shmadd;

    /* 建立共享内存 */
    if((shmid=shmget(IPC_PRIVATE,BUFSZ,0666))<0){
        perror("shmget");
        exit(1);
    }else
        printf("create share-memofy:%d\n",shmid);

    system("ipcs -m");
    
    /* 映射共享内存 */
    if((shmadd=shmat(shmid,0,0))<(char *)0){
        perror("shmat");
        exit(1);
    }else
        printf("attached shared_memory\n");

    /* 显示系统内存状况 */
    system("ipcs -m");

    /* 删除共享内存 */
    if((shmdt(shmadd))<0){
        perror("shmdt");
        exit(1);
    }else
        printf("deleted shared-memory\n");

    system("ipcs -m");
    exit(0);
        
}
shmadd.c

  运行结果:

abc@pc:~/c/app$ ./shmadd
create share-memofy:1474571

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 65536      abc        600        524288     2          dest
......
0x00000000 1441802    abc        600        524288     2          dest
0x00000000 1474571    abc        666        2048       0

attached shared_memory

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 65536      abc        600        524288     2          dest
......
0x00000000 1441802    abc        600        524288     2          dest
0x00000000 1474571    abc        666        2048       1

deleted shared-memory

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 65536      abc        600        524288     2          dest
......
0x00000000 1441802    abc        600        524288     2          dest
0x00000000 1474571    abc        666        2048       0
shmadd.c

 

8.5 消息队列

  8.5.1 消息队列概述

  顾名思义,消息队列就是一个消息的列表。用户能够从消息队列中添加消息、读取消息等。从这点上看,消息队列具备必定的FIFO的特性,可是它能够实现消息的随机查询,比FIFO具备更大的优点。同时,这些消息优点存在于内核中的,有“队列ID”来标识。

  8.5.2 消息队列实现

   1. 函数说明

  消息队列的实现包括建立或打开消息队列、添加消息、读取消息和控制消息队列这四种操做。其中建立或打开消息队列使用的函数是msgget,这里建立的消息对队列的数量会受到系统消息队列数量的限制;添加消息使用的函数是msgsnd函数,它把消息添加到已打开的消息队列末尾;读取消息使用的函数是msgrcv,它把消息从消息队列中取出,与FIFO不一样的是,这里能够指定取走某一条消息;最后控制消息队列使用的函数是msgctl,它能够完成多项功能。

  2. 函数格式

  msgget

所需头文件 #include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
函数原型 int msgget(key_t key,int flag)
函数传入值 key:返回新的或已有队列ID,IPC_PRIVATE
flag
函数返回值 成功:消息队列ID
出错:-1

   msgsnd

所需头文件 #include<sys/types.h>
#include<sys/ipc.h>
#inlcude<sys/shm.h>
函数原型 int msgsnd(int msqid,const void *prt,size_t size,int falg)
函数传入值 msqid:消息队列的队列ID

prt:指向消息结构的指针。该消息的结果msgbuf为:

struct msgbuf{

long mtype;//消息类型

char mtext[1];//消息正文

}

size:消息的字节数,不要以null结尾
flag IPC_NOWAIT 若消息并无当即发送而调用进程会当即返回
0:msgsnd调用阻塞直到条件知足为止
函数返回值 成功:0
出错:-1

   msgrcv

所需头文件 #inlcude<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
函数原型 int msgrcv(int msgid,msbuf* msgp,int size,long msgtype,int flag)
函数传入值 msgid:消息队列的队列ID
msgp:消息缓冲区
size:消息的字节数,不要以null结尾
msgtype 0:接收消息队列中第一个消息
大于0:接收消息队列中第一个类型为msgtyp的消息
小于0:接收消息队列中第一个类型值不小于msgtyp绝对值且类型值又最小的消息
flag MSG_NOERROR:若返回的消息比size字节多,则消息就会截断奥size字节,且不通知消息发送进程
IPC_NOWAIT:若消息并无当即发送而调用进程会当即返回
0:msgsnd调用阻塞直到条件知足为止
函数返回值 成功:0
出错:-1

  

  msgctl

所需头文件 #include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
函数原型 int msgctl(int msgqid,int cmd,struct msqid_ds *buf)
函数传入值 msqid:消息队列的队列ID
cmd IPC_STAT:读取消息队列的数据结果msqid_ds,并将其存储在buf指定的地址中
IPC_SET:设置消息队列的数据结果msqid_ds中的ipc_perm元素的值。这个值取自buf参数。
IPC_RMID:从系统内核中移走消息队列
buf:消息队列缓冲区
函数返回值 成功:0
出错:-1

 

   3.使用实例

   这个实例体现了如何使用消息队列进行进程间通讯,包括消息队列的建立消息发送与读取消息队列的撤销等多种操做。注意这里使用了函数ftok,它能够根据不一样的路径和关键表示产生标准的key。

#include<sys/ipc.h>
#include<sys/msg.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>

#define BUFSZ 512

struct message {
    long msg_type;
    char msg_text[BUFSZ];
};

int main()
{
    int qid;
    key_t key;
    int len;
    struct message msg;

    /* 根据不一样的路径和关键表示产生标准的key */
    if((key=ftok(".",'a'))==-1){
        perror("ftok");
        exit(1);
    }

    /* 建立消息队列 */
    if((qid=msgget(key,IPC_CREAT|0666))==-1){
        perror("msgget");
        exit(1);
    }
    printf("opened queue %d\n",qid);
    puts("Please enter the message to queue:");
    if((fgets((&msg)->msg_text,BUFSZ,stdin))==NULL){
        puts("no message");
        exit(1);
    }
    msg.msg_type = getpid();
    len = strlen(msg.msg_text);

    /* 添加消息队列 */
    if((msgsnd(qid,&msg,len,0))<0){
        perror("message posted");
        exit(1);
    }

    /* 读取消息队列 */
    if(msgrcv(qid,&msg,BUFSZ,0,0)<0){
        perror("msgrcv");
        exit(1);
    }
    printf("message is:%s\n",(&msg)->msg_text);

    /* 从系统内核中移走消息队列 */
    if((msgctl(qid,IPC_RMID,NULL))<0){
        perror("msgctl");
        exit(1);
    }
    exit(0);
}
msg.c

  运行:

abc@pc:~/c/app$ ./msg
opened queue 0
Please enter the message to queue:
hello
message is:hello

abc@pc:~/c/app$ ./msg
opened queue 32768
Please enter the message to queue:
world
message is:world
msg.c
相关文章
相关标签/搜索