进程间通信
进程间通信
在进程控制开发中,读者已经学会了如何创建进程以及如何对进程进行基本的控制,这些都只是停留在父子进程之间的控制,本章将样学习不同的进程间进行通信的方法。通过本章学习,读者将会掌握以下内容:
- 掌握Linux中管道的基本概念
- 掌握Linux中管道的创建
- 掌握Linux中管道的读写
- 掌握Linux中有名管道的创建读写方法
- 掌握Linux中消息队列的处理
- 掌握Linux中共享内存的处理
8.1 Linux下进程间通信概述
在上一章中,读者已经知道了进程是一个程序的依稀执行的过程。这里所说的进程一般是指运行在用户态的进程,而由于处于用户态的不同进程之间是彼此隔离的,就像处于不同城市的人们,它们必须通过某种方式来提供通信,例如人们现在广泛使用的手机等方式。本章就是讲述如何建立这些不同的通话方式,就像人们有很多种通信方式一样。
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中使用较多的进程间通信方式主要有一下几种。
(1) 管道(pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信;有名管道,除具有管道所具有的的功能外,它还允许无亲缘关系进程间的通信。
(2) 信号(signal):信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知接受进程有某个事件发生,一个进程收到一个信号与处理器收到一个中断请求的效果上可以说是一样的。
(3) 消息队列:消息队列是消息的链接表,包括Posix消息队列、SystemV消息队列。它克服了前两种通信方式中信息量有限的缺点,具有写权限的进程可以向消息队列中按照一定的规则添加新消息:对消息队列有读权限的进程则可以从消息队列中读取消息。
(4) 共享内存:可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。
(5) 信号量:主要作为进程间以及同一进程不同线程之间的同步手段。
(6) 套接字(Socket):这是一种更为一般的进程间通信机制,它可用于不同机器之间的进程间通信,应用非常广泛。
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]); }
程序运行后先成功创建一个无名管道,之后再将其关闭。
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); } }
运行结果:
parent write1 success! parent write2 success! 10 numbers read from the pipe is Hello Pipe
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); }
运行结果:
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
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); }
#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); }
为了能更好地观察运行结果,需要把这两个程序分别在两个终端里运行,在这里首先启动读管道程序。由于这是非阻塞管道,因此在建立管道之后程序就开始循环从管道里读出内容。在启动了写管道程序后,读进程能够从读管道里读出用户的输入内容,程序运行结果如下:
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
abc@pc:~/c/app$ ./fifo_write 123456 write 123456 to the FIFO abc@pc:~/c/app$ ./fifo_write Hello write Hello to the FIFO
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"); } } } }
运行结果:
abc@pc:~/c/app$ ./kill pid=20460 kill 20460 abc@pc:~/c/app$ ./kill pid=20462 kill 20462
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); }
运行结果:
abc@pc:~/c/app$ ./alarm
Alarm clock
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); }
运行结果:
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
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); } } }
运行结果:
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$
可见,在信号处于阻塞状态是,所发出的信号对进程不起做。读者等待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); }
运行结果:
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
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); }
运行:
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