进程间通讯(Interprocess Communication,IPC)是一个描述两个进程彼此交换信息的通用术语。通常状况下,通讯的两个进程便可以运行在同一台机器上,也能够运行在不一样的机器上。进程间的通讯是数据的交换,两个或多个进程合做处理数据或同步信息,以帮助两个彼此独立但相关联的进程调度工做,避免重复工做。进程间通讯方式有不少种,好比可使用socket、使用管道、消息队列、文件、共享内存等。php
管道是进程间通讯中最古老的方式,他使得数据以一种数据流的方式在多个进程之间流动。管道至关于文件系统上的一个文件,用来缓存所要传输的数据,可是在某些特性上又不一样于文件,例如,当数据读出后,管道中的数据就没有了, 单文件就没有这个特性。综合来讲,管道具备如下特色:html
1) 管道是半双工的,数据只能向一个方向流动;须要双方通讯时,须要创建起两个管道;node
2) 匿名管道只能用于父子进程或者兄弟进程之间(具备亲缘关系的进程);linux
3) 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,而且只存在与内存中。程序员
管道分为pipe(无名管道)和fifo(命名管道)两种,除了创建、打开、删除的方式不一样外,这两种管道几乎是同样的。他们都是经过内核缓冲区实现数据传输。前者用于父进程和子进程间的通讯,后者用于运行于同一台机器上的任意两个进程间的通讯编程
Linux下使用pipe()建立一个匿名半双工管道,其函数原型以下:数组
1 #include <unistd.h> 2 int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1
参数fd是一个长度为2的文件描述符数组,fd[0]是读出端,fd[1]是写入端,函数返回0表示成功,返回-1则表示失败。缓存
但函数返回成功,则代表自动维护了一个从fd[1]到fd[0]的数据通道。session
要关闭管道只需将这两个文件描述符关闭便可。数据结构
单独操做一个进程管道是没有意义的,管道的应用通常体如今父子进程或者兄弟进程之间的通讯上。若是要创建一个父进程到子进程的数据通道,须要先调用函数pipe(),紧接着调用函数fork(),因为子程序自动继承父进程的数据段,则子进程同时拥有管道的操做权,此时管道的方向取决于用户怎么维护该管道。
当用户想要一个父进程的数据管道是,须要先在父进程中关闭管道的独处端,而后相应的在子进程中关闭管道的输出端,相反,当维护子进程到父进程的数据通道时,则须要在父进程中关闭输出端,在子进程中关闭读入端便可。总之,使用函数pipe()和fork()建立子进程,维护父子进程中管道的数据方法是:在父进程中向子进程发送消息,在子进程接受消息。
若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0]
)与子进程的写端(fd[1]
);反之,则可使数据流从子进程流向父进程。
#include<stdio.h> #include<unistd.h> int main() { int fd[2]; // 两个文件描述符 pid_t pid; char buff[20]; if(pipe(fd) < 0) // 建立管道 printf("Create Pipe Error!\n"); if((pid = fork()) < 0) // 建立子进程 printf("Fork Error!\n"); else if(pid > 0) // 父进程 { close(fd[0]); // 关闭读端 write(fd[1], "hello world\n", 12); } else { close(fd[1]); // 关闭写端 read(fd[0], buff, 20); printf("%s", buff); } return 0; }
程序运行结果以下:
FIFO(First Input First Output)是一种文件类型,在文件系统中能够看到。经过FIFO,不相关的进程也能交换数据。
FIFO的通讯方式相似于在进程中使用文件类传输数据,只不过FIFO类型的文件同时具备管道的特性,在数据读出时,FIFO中同时清除了数据。
建立FIFO相似于建立文件,FIFO就像普通文件同样,也能够经过路径名进行访问。Linux系统提供了函数mkfifo(),用于建立FIFO,函数原型以下:
#include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode); int mkfifoat(int dirfd, const char *pathname, mode_t mode);
函数mkfifo()中参数mode的规格说明与函数open()中的参数mode的规格说明相同。函数mkfifoat()与函数mkfifo()类似,可是函数mkfifoat()能够被用来在文件描述符dirfd表示的目录相关位置建立一个有名管道,有如下三种情形:
(1)若是参数pathname指定的是绝对路径名,则参数dirfd会被忽略,而且函数mikfifoat()的行为和函数mkfifo()的行为相似。
(2)若是参数pathname指定是相对相对路径名,则参数dirfd是一个打开目录的有效文件描述符,路径名和目录相关。
(3)若是参数pathname 制定的是相对路径名,则参数dirfd是一个特殊值AT_FDCWD,则路径名以当前目录开始,函数mkfifoat()于mkfifo()相似。
当使用函数open()打开一个有名管道时,非阻塞标识(O_NONBLOCK)会产生下列影响:
(1)在通常状况下(没有制定O_NONBLOCK),只读open()要阻塞到某个进程为写而打开这个FIFO为止;相似的,只写open()要阻塞到某个其余进程为读而打开它为止。
(2)若是指定了O_NONBLOCK,则只读当即返回;可是若是没有进程为读而打开一个FIFO,那么只写open()将返回-1,同时errno设置为ENXIO。
相似于管道,若写一个尚无进程为读而打开的FIFO,将产生信号SIGPIPE,若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程将产生一个文件结束标志。
示例:
编写fifo_write.c和fifo_read.c以下
fifo_write.c
#include<stdio.h> #include<stdlib.h> // exit #include<fcntl.h> // O_WRONLY #include<sys/stat.h> #include<time.h> // time int main() { int fd; int n, i; char buf[1024]; time_t tp; printf("I am %d process.\n", getpid()); // 说明进程ID if((fd = open("fifo1", O_WRONLY)) < 0) // 以写打开一个FIFO { perror("Open FIFO Failed"); exit(1); } for(i=0; i<10; ++i) { time(&tp); // 取系统当前时间 n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp)); printf("Send message: %s", buf); // 打印 if(write(fd, buf, n+1) < 0) // 写入到FIFO中 { perror("Write FIFO Failed"); close(fd); exit(1); } sleep(1); // 休眠1秒 } close(fd); // 关闭FIFO文件 return 0; }
fifo_read.c
#include<stdio.h> #include<stdlib.h> #include<errno.h> #include<fcntl.h> #include<sys/stat.h> int main() { int fd; int len; char buf[1024]; if(mkfifo("fifo1", 0666) < 0 && errno!=EEXIST) // 建立FIFO管道 perror("Create FIFO Failed"); if((fd = open("fifo1", O_RDONLY)) < 0) // 以读打开FIFO { perror("Open FIFO Failed"); exit(1); } while((len = read(fd, buf, 1024)) > 0) // 读取FIFO管道 printf("Read message: %s", buf); close(fd); // 关闭FIFO文件 return 0; }
建立一个空的fifo1的文件后,先运行fifo_write,再运行fifo_read可获得以下运行结果
信号是Linux系统响应某些条件而产生的一个事件,是进程间通讯的经典方法。Linux有不少种信号,每一个信号都有一个名字,这些名字都以三个字符SIG开头,经常使用的信号量以下表所示,可使用Shell命令kill -l查看当前系统提供的信号。
Signal
|
Description
|
SIGABRT
|
由调用abort函数产生,进程非正常退出
|
SIGALRM
|
用alarm函数设置的timer超时或setitimer函数设置的interval timer超时
|
SIGBUS
|
某种特定的硬件异常,一般由内存访问引发
|
SIGCANCEL
|
由Solaris Thread Library内部使用,一般不会使用
|
SIGCHLD
|
进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省状况下该Signal会被忽略
|
SIGCONT
|
当被stop的进程恢复运行的时候,自动发送
|
SIGEMT
|
和实现相关的硬件异常
|
SIGFPE
|
数学相关的异常,如被0除,浮点溢出,等等
|
SIGFREEZE
|
Solaris专用,Hiberate或者Suspended时候发送
|
SIGHUP
|
发送给具备Terminal的Controlling Process,当terminal被disconnect时候发送
|
SIGILL
|
非法指令异常
|
SIGINFO
|
BSD signal。由Status Key产生,一般是CTRL+T。发送给全部Foreground Group的进程
|
SIGINT
|
由Interrupt Key产生,一般是CTRL+C或者DELETE。发送给全部ForeGround Group的进程
|
SIGIO
|
异步IO事件
|
SIGIOT
|
实现相关的硬件异常,通常对应SIGABRT
|
SIGKILL
|
没法处理和忽略。停止某个进程
|
SIGLWP
|
由Solaris Thread Libray内部使用
|
SIGPIPE
|
在reader停止以后写Pipe的时候发送
|
SIGPOLL
|
当某个事件发送给Pollable Device的时候发送
|
SIGPROF
|
Setitimer指定的Profiling Interval Timer所产生
|
SIGPWR
|
和系统相关。和UPS相关。
|
SIGQUIT
|
输入Quit Key的时候(CTRL+\)发送给全部Foreground Group的进程
|
SIGSEGV
|
非法内存访问
|
SIGSTKFLT
|
Linux专用,数学协处理器的栈异常
|
SIGSTOP
|
停止进程。没法处理和忽略。
|
SIGSYS
|
非法系统调用
|
SIGTERM
|
请求停止进程,kill命令缺省发送
|
SIGTHAW
|
Solaris专用,从Suspend恢复时候发送
|
SIGTRAP
|
实现相关的硬件异常。通常是调试异常
|
SIGTSTP
|
Suspend Key,通常是Ctrl+Z。发送给全部Foreground Group的进程
|
SIGTTIN
|
当Background Group的进程尝试读取Terminal的时候发送
|
SIGTTOU
|
当Background Group的进程尝试写Terminal的时候发送
|
SIGURG
|
当out-of-band data接收的时候可能发送
|
SIGUSR1
|
用户自定义signal 1
|
SIGUSR2
|
用户自定义signal 2
|
SIGVTALRM
|
setitimer函数设置的Virtual Interval Timer超时的时候
|
SIGWAITING
|
Solaris Thread Library内部实现专用
|
SIGWINCH
|
当Terminal的窗口大小改变的时候,发送给Foreground Group的全部进程
|
SIGXCPU
|
当CPU时间限制超时的时候
|
SIGXFSZ
|
进程超过文件大小限制
|
SIGXRES
|
Solaris专用,进程超过资源限制的时候发送
|
当引起信号的时间发生时,为进程产生一个信号,有如下两种状况:
(1)硬件状况:例如按下键盘或其余硬件故障
(2)软件产生:例如除0操做或者执行kill()函数、raise()函数等。
一个完整的信号周期包括信号的产生、信号在进程内的注册与注销以及执行信号处理的三个阶段。进程收到信号后有三种处理方式:
(1)捕捉信号:当信号发生时,进程可执行相应的自处理函数。
(2)忽略信号:对该信号不作任何处理,但SIGKILL与SIGSTOP信号除外。
(3)执行默认操做:Linux对每种信号都规定了默认操做。
下面是信号操做中经常使用的函数:
函数signal()进行信号处理时,须要指出要处理的信号和处理函数信息,其函数原型以下:
#include <signal.h> typedef void (*sighandler_t)(int) sighandler_t signal(int signum,sighandler_t handler);
函数执行成功,返回之前的信号处理配置或者处理函数;执行失败返回SIGERR即-1。
参数signum用于指定待响应的信号;参数handler为信号处理函数,有如下三种状况:
(1)SIG_IGN:忽略该信号。
(2)SIG_DFL:默认方式为处理该信号。
(3)自定义信号处理函数指针,返回类型为void。
例子:下面看一个简单的捕捉SIGUSR1信号的处理函数。
#include<stdio.h> #include<signal.h> #include<unistd.h> void sig_handler(int signo) { if (signo == SIGINT) printf("received SIGINT\n"); } int main(void) { if (signal(SIGINT, sig_handler) == SIG_ERR) printf("\ncan't catch SIGINT\n"); // A long long wait so that we can easily issue a signal to this process while(1) sleep(1); return 0; }
在上面的代码中,咱们使用无限循环模拟了一个长时间运行的进程。函数sig_handler用做信号处理程序。经过在main()函数中将系统调用'signal'做为第二个参数传递给内核,该函数被注册到内核。函数'signal'的第一个参数是咱们但愿信号处理程序处理的信号,在这种状况下是SIGINT。
在其后,函数sleep(1)的使用有一个缘由。这个函数已经在while循环中使用,以便while循环在一段时间后执行(在这种状况下,即1秒)。这变得很重要,不然无限循环运行可能会消耗大部分CPU,会使计算机很是慢。
当进程运行,信号SIGINT由按下Ctrl-C发出,信号SIGQUIT由按下Ctrl-发出,能够获得以下结果:
须要提到的是signal函数是一个比较老的函数,在实际应用中应该避免使用该函数,而使用sigaction函数,后面将详细介绍这个函数。
函数sigaction与函数signal功能相似,主要用于定义在接收到信号后应该采起的处理方式,其函数原型以下:
#include <signal.h> int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,能够为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义本身的处理函数,将致使信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,能够为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存返回的原来对相应信号的处理,可指定oldact为NULL。若是把第2、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。
第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程当中应屏蔽掉哪些信号等等。
sigaction结构定义以下:
struct sigaction { union{ __sighandler_t _sa_handler; void (*_sa_sigaction)(int,struct siginfo *, void *); }_u sigset_t sa_mask; unsigned long sa_flags; }
其中,结构体的关键成员含义
一、联合数据结构中的两个元素_sa_handler以及*_sa_sigaction指定信号关联函数,即用户指定的信号处理函数。除了能够是用户自定义的处理函数外,还能够为SIG_DFL(采用缺省的处理方式),也能够为SIG_IGN(忽略信号)。
二、由_sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(固然一样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用,第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构以下:
siginfo_t { int si_signo; /* 信号值,对全部信号有意义*/ int si_errno; /* errno值,对全部信号有意义*/ int si_code; /* 信号产生的缘由,对全部信号有意义*/ union{ /* 联合数据结构,不一样成员适应不一样信号 */ //确保分配足够大的存储空间 int _pad[SI_PAD_SIZE]; //对SIGKILL有意义的结构 struct{ ... }... ... ... ... ... //对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构 struct{ ... }... ... ... } }
前面在讨论系统调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,当调用sigqueue时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可让信号传递一些附加信息。信号能够传递信息对程序开发是很是有意义的。
三、sa_mask指定在信号处理程序执行过程当中,哪些信号应当被阻塞。缺省状况下当前信号自己被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。
注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程当中由sa_mask指定的信号才被阻塞。
四、sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另外一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数能够被传递到信号处理函数中,所以,应该为sigaction结构中的sa_sigaction指定处理函数,而不该该为sa_handler指定信号处理函数,不然,设置该标志变得毫无心义。即便为sa_sigaction指定了信号处理函数,若是不设置SA_SIGINFO,信号处理函数一样不能获得信号传递过来的数据,在信号处理函数中对这些信息的访问都将致使段错误(Segmentation fault)。
函数kill()用于向自身或其余进程发送信号,函数原型以下:
#include <sys/types.h> #include <signal.h> int kill(pid_t pid,int signo)
参数sig用于指定要发送的信号。参数pid用于指定目标进程,设定值以下:
pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 pid!=-1 进程组ID为 -pid的全部进程
pid=-1 除发送进程自身外,全部进程ID大于1的进程
Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,所以,可用于检查目标进程是否存在,以及当前进程是否具备向目标发送信号的权限(root权限的进程能够向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。
Kill()最经常使用于pid>0时的信号发送。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码:
EINVAL:指定的信号sig无效。
ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,多是一个尚未被wait收回,但已经终止执行的僵死进程。
EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。由于,一个进程被容许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。若是参数pid小于-1,即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。
函数raise()用于进程向自身发送信号,其函数原型以下:
#include <signal.h> int raise(int signo)
参数signo为即将发送的信号值。调用成功返回 0;不然,返回 -1。
函数pause()用于将调用进程挂起直至捕捉到信号为止,一般用于判断信号是否到达,其函数原型以下:
#include <unistd.h> int pause(void);
sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(固然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。
#include <sys/types.h> #include <signal.h> int sigqueue(pid_t pid, int sig, const union sigval val)
sigqueue的第一个参数是指定接收信号的进程ID,第二个参数肯定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即一般所说的4字节值。
typedef union sigval {
int sival_int;
void *sival_ptr;
}sigval_t;
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。若是signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。
在调用sigqueue时,sigval_t指定的信息会拷贝到对应sig 注册的3参数信号处理函数的siginfo_t结构中,这样信号处理函数就能够处理这些信息了。因为sigqueue系统调用支持发送带参数信号,因此比kill()系统调用的功能要灵活和强大得多。
函数alarm()也称为闹钟函数,是专门为信号SIGALARM而设的,用于在指定的时间项进程自己发送SIGALARM信号,其函数原型以下:
#include <unistd.h>
unsigned int alarm(unsigned int seconds)
若是指定的参数seconds为0,则再也不发送 SIGALRM信号。后一次设定将取消前一次的设定。该调用返回值为上次定时调用到发送之间剩余的时间,或者由于没有前一次定时调用而返回0。
如今的系统中不少程序再也不使用alarm调用,而是使用setitimer调用来设置定时器,用getitimer来获得定时器的状态,这两个调用的声明格式以下:
#include <sys/time.h> int getitimer(int which, struct itimerval *value); int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器从新开始。三个计时器由参数which指定,以下所示:
TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。
ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。
ITIMER_PROF:当进程执行时和系统为该进程执行动做时都计时。与ITIMER_VIR-TUAL是一对,该定时器常常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程。
定时器中的参数value用来指明定时器的时间,其结构以下:
struct itimerval { struct timeval it_interval; /* 下一次的取值 */ struct timeval it_value; /* 本次的设定值 */ };
该结构中timeval结构定义以下:
struct timeval { long tv_sec; /* 秒 */ long tv_usec; /* 微秒,1秒 = 1000000 微秒*/ };
在setitimer 调用中,参数ovalue若是不为空,则其中保留的是上次调用设定的值。定时器将it_value递减到0时,产生一个信号,并将it_value的值设定为it_interval的值,而后从新开始计时,如此往复。当it_value设定为0时,计时器中止,或者当它计时到期,而it_interval 为0时中止。调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno:
EFAULT:参数value或ovalue是无效的指针。
EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。
下面是关于setitimer调用的一个简单示范,在该例子中,每隔一秒发出一个SIGALRM,每隔0.5秒发出一个SIGVTALRM信号:
#include <signal.h> #include <unistd.h> #include <stdio.h> #include <sys/time.h> int sec; void sigroutine(int signo) { switch (signo) { case SIGALRM: printf("Catch a signal -- SIGALRM "); break; case SIGVTALRM: printf("Catch a signal -- SIGVTALRM "); break; } return; } int main() { struct itimerval value,ovalue,value2; sec = 5; printf("process id is %d ",getpid()); signal(SIGALRM, sigroutine); signal(SIGVTALRM, sigroutine); value.it_value.tv_sec = 1; value.it_value.tv_usec = 0; value.it_interval.tv_sec = 1; value.it_interval.tv_usec = 0; setitimer(ITIMER_REAL, &value, &ovalue); value2.it_value.tv_sec = 0; value2.it_value.tv_usec = 500000; value2.it_interval.tv_sec = 0; value2.it_interval.tv_usec = 500000; setitimer(ITIMER_VIRTUAL, &value2, &ovalue); for (;;) ; }
程序运行结果以下:
向进程发送SIGABORT信号,默认状况下进程会异常退出,固然可定义本身的信号处理函数。
#include <stdlib.h> void abort(void);
即便SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。
信号集就是用来放置多个信号,和select函数中的描述符集类似。系统也提供了一系列的信号集函数,这些函数原型以下:
#include <signal.h> int sigemptyset(sigset_t *set);//清空信号集set int sigfillset(sigset_t *set);//将全部信号填充到信号集set,set指向的信号集中将包含linux支持的64种信号; int sigaddset(sigset_t *set, int signum)//在set指向的信号集中加入signum信号; int sigdelset(sigset_t *set, int signum);//在set指向的信号集中删除signum信号; int sigismember(const sigset_t *set, int signum);//断定信号signum是否在set指向的信号集中。
sigprocmask函数能够检测或更改(或二者)进程的信号屏蔽字,函数原型以下:
int sigpromask(int how,const sigset_t* set,sigset_t* oset);
参数oset,输出参数,若非空,则返回进程的当前屏蔽字。
参数set,输入参数,若非空,则表示须要修改的信号屏蔽字。
参数how,输入参数,表示以何种方式修改当前信号屏蔽字。若是set为空,则how无心义。
参数how的取值有:
(1)SIGBLOCK 该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含了咱们但愿阻塞的附加信号。
(2)SIGUBLOCK 该进程新的心啊后屏蔽字是当前信号除去set所指向的信号集。set包含了咱们但愿解除阻塞的信号。
(3)SIGSETMASK 赋值操做,该进程新的信号屏蔽字是set指向的值。
sigsuspend函数就是在捕捉一个信号或发生了一个会终止该进程的信号以前,将进程投入睡眠,直到该信号来到并从信号处理函数中返回。sigsuspend函数原型以下:
int sigsuspend(const sigset_t *mask));
参数sigmask,将进程的信号屏蔽字设置为sigmask,也就是说进程会在睡眠后的信号屏蔽字。
所以在使用sigsuspend函数时,当该函数返回后,应该将进程原来的屏蔽字再从新设置回去。
sigpending函数返回在送往进程的时候被阻塞挂起的信号集合。函数原型为:
int sigpending(sigset_t *set)
sigpending(sigset_t *set))得到当前已递送到进程,却被阻塞的全部信号,在set指向的信号集中返回结果。
经过一个实例进行理解
#include<stdlib.h> #include<stdio.h> #include<unistd.h> #include<signal.h> #include<sys/types.h> void print_sigset(sigset_t *set) { int i; for(i=1;i<64;++i) { if(sigismember(set,i)) { printf("1"); }else{ printf("0"); } } } int main() { sigset_t myset; sigemptyset(&myset);//清空信号集 sigaddset(&myset,SIGINT);//向信号集添加 sigaddset(&myset,SIGQUIT); sigaddset(&myset,SIGUSR1); print_sigset(&myset); return 0; }
信号集运行结果如图
消息队列是消息的连接表,存放在内核中并由消息队列标识符标识。消息队列与FIFO有许多类似之处,可是少了管道打开文件和关闭文件的麻烦。它可用于不一样进程间的通讯,可是其重点仍是线程之间的一种通讯方式。如今首先详细对进程间的通讯进行讲解。
一、msgget()
msgget用来建立和访问一个消息队列,函数原型以下:
#include <sys/types.h>
#include <sys/ipc.h> #include <sys/msg.h> int msgget ( key_t key , int msgflg );
与其余的IPC机制同样,程序必须提供一个键来命名某个特定的消息队列。msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限同样。msgflg能够与IPC_CREAT作或操做,表示当key所命名的消息队列不存在时建立一个消息队列,若是key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符。
它返回一个以key命名的消息队列的标识符(非零整数),失败时返回-1.
二、msgsnd()
该函数用来把消息添加到消息队列中。它的原型为:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
msgid是由msgget函数返回的消息队列标识符。
msg_ptr是一个指向准备发送消息的指针,可是消息的数据结构却有必定的要求,指针msg_ptr所指向的消息结构必定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来肯定消息的类型。因此消息结构要定义成这样:
struct my_message { long int message_type; /* The data you wish to transfer */ };
msg_sz 是msg_ptr指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msg_sz是不包括长整型消息类型成员变量的长度。
msgflg 用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。
若是调用成功,消息数据的一分副本将被放到消息队列中,并返回0,失败时返回-1.
三、msgrcv()
从消息队列中读取以及删除一条消息,并将内容复制进MSGP指向的缓冲区中,其函数原型以下:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgrcv( int msgid , struct msgbuf* msgp , int msgsz , long msgtyp, int msgflg); 成功时返回所获取信息的长度,失败返回-1,错误信息存于error
msgid, msg_ptr, msg_st 的做用也函数msgsnd()函数的同样。
msgtyp: 信息类型。 取值以下:
msgtyp = 0 ,不分类型,直接返回消息队列中的第一项 。
msgtyp > 0 ,返回第一项 msgtyp与 msgbuf结构体中的mtype相同的信息 。
msgtyp <0 , 返回第一项 mtype小于等于msgtyp绝对值的信息。
msgflg 用于控制当队列中没有相应类型的消息能够接收时将发生的事情。
调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,而后删除消息队列中的对应消息。失败时返回-1。
四、msgctl()函数
该函数用来控制消息队列,它与共享内存的shmctl函数类似,它的原型为:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h>
int msgctl(int msgid, int command, struct msgid_ds *buf);
command是将要采起的动做,它能够取3个值,
buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。msgid_ds结构至少包括如下成员:
struct msgid_ds { uid_t shm_perm.uid; uid_t shm_perm.gid; mode_t shm_perm.mode; };
示例:消息队列进行进程通讯。
msg_client.c
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/stat.h> #define MSG_FILE "msg_server.c" #define BUFFER 255 #define PERM S_IRUSR|S_IWUSR struct msgtype { long mtype; char buffer[BUFFER+1]; }; int main(int argc,char **argv) { struct msgtype msg; key_t key; int msgid; if(argc!=2) { fprintf(stderr,"Usage:%s string\n\a",argv[0]); exit(1); } if((key=ftok(MSG_FILE,'a'))==-1) { fprintf(stderr,"Creat Key Error:%s\a\n",strerror(errno)); exit(1); } if((msgid=msgget(key,PERM))==-1) { fprintf(stderr,"Creat Message Error:%s\a\n",strerror(errno)); exit(1); } msg.mtype=1; strncpy(msg.buffer,argv[1],BUFFER); msgsnd(msgid,&msg,sizeof(struct msgtype),0); memset(&msg,'\0',sizeof(struct msgtype)); msgrcv(msgid,&msg,sizeof(struct msgtype),2,0); fprintf(stderr,"Client receive:%s\n",msg.buffer); exit(0); }
msg_server.c
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/stat.h> #include <sys/msg.h> #define MSG_FILE "msg_server.c" #define BUFFER 255 #define PERM S_IRUSR|S_IWUSR struct msgtype { long mtype; char buffer[BUFFER+1]; }; int main() { struct msgtype msg; key_t key; int msgid; if((key=ftok(MSG_FILE,'a'))==-1){ fprintf(stderr,"Creat Key Error:%s\a\n",strerror(errno)); exit(1); } if((msgid=msgget(key,PERM|IPC_CREAT|IPC_EXCL))==-1) { fprintf(stderr,"Creat Message Error:%s\a\n",strerror(errno)); exit(1); } while(1) { msgrcv(msgid,&msg,sizeof(struct msgtype),1,0); fprintf(stderr,"Server Receive:%s\n",msg.buffer); msg.mtype=2; msgsnd(msgid,&msg,sizeof(struct msgtype),0); } exit(0); }
程序编译运行结果以下:
信号量是进程间通讯以前进行进程间同步必需要掌握的内容,信号量(Semaphore)是一种用于提供不一样进程间或一个给定进程的不一样线程间同步手段的原语,以防止一个进程在访问共享内存的同时拎一个进程更新这块内存的状况。POSIX有两种信号量:一种是基于名字的信号量,一种是基于内存的信号量。这两种信号量均可以用于不一样进程或者不一样线程之间的同步。
基于名字的信号量,是一种自然的适用于不一样进程的同步。由于这种所谓的名字和文件系统进行了关联,这样就使得各进程均可以访问(其实未必真正的和实际的文件相关联,只不过对于应用来说,用起来就像文件同样)。
基于内存的信号量,若是放置在进程间的共享内存中,既能够进行进程间的同步。尽管这两种信号两均可以用于进程或者线程之间的同步,但原则上基于名字的信号量更多的应用于不一样进程间的同步,而基于内存的信号量更多的用于线程间的同步。
为了得到共享资源,进程须要执行下列操做:
(1)测试控制该资源的信号量。
(2)若信号量的值为正,则进程可使用该资源,这种状况下,进程会将信号量的值减1,表示它使用了一个资源单位。
(3)若信号量的值为负,则进程进入休眠状态,直至信号量值大于0;进程被唤醒后,返回步骤1.
(4)当进程再也不使用由一个信号量控制的共享资源时,该信号量增1;若是有进程正在休眠等待此信号,则唤醒它们。
为了正确实现信号量,信号量值的测试及减1操做应当是原子操做,为此,信号量一般是在内核中实现的。
经常使用的信号量形式被称为二元信号量,它控制单个资源,其初始值为1.可是通常而言,信号量的初值能够是任意一个正值,改值代表有多少个共享资源单位可共享应用。
信号量的等待和挂出:
函数semget()用于建立一个新信号量集或获取一个既有集合的标识符,其函数原型以下:
#include<sys/types.h> #include <sys/sem.h> #include <sys/ipc.h> int semget(key_t key,int nsems,int semflg);
函数semget()执行成功,返回新信号量集或即有信号量集的标识符,后续引用单个信号量的系统调用必需要同时指定信号量集标识符和信号量在集合中的序号,一个集合中的信号量从0开始计数;执行失败则返回-1.
参数key用于指定信号量集的名称,其特殊键值IPC_PRIVATE的做用是建立一个仅能由本进程访问的私用信号量集。
参数semflg用于指定信号量集的访问权限,由9个权限标识构成。经过指定的IPC_CREAT标志来建立一个消息队列,若由参数标识的信号集已经存在,就返回已有信号量集,忽略IPC_CREAT的标识做用。
参数key |
参数sem_flg |
semget调用结果 |
errno信息 |
IPC_PRIVATE |
无要求 |
成功 |
无 |
不存在相同key |
IPC_CREAT|权限 |
成功 |
无 |
存在相同key |
IPC_CREAT| IPC_EXCL|权限值 |
失败 |
EEXIST |
存在相同key |
IPC_CREAT|权限 |
成功 |
无
|
函数semctl用于在一个信号量集或集合中的单个信号量上执行各类操做控制,其函数原型以下:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid,int semnum,int cmd,...);
参数semctl()是操做所施加的信号量集的标识符。对于那些在单个信号量上执行的操做,参数semnum标识出了集合中的具体信号量,对于其余操做则会忽略这个参数,而且能够将其设置为0.参数cmd指定了需执行的控制操做,常规控制操做以下:
IPC_RMID:当即删除信号量集及其相关联的semid_ds数据结构。全部因在函数semop调用中等待这个集合中的信号量而阻塞的进程都会当即被唤醒,函数semop()会报告错误EIDRM,这个操做无需参数。
IPC_STAT:在所指向的缓冲区中放置一份与这个信号量集相关联的semdi_ds数据结构的副本。
IPC_SET:使用所指向的缓冲区中的值来更新与这个信号量集相关联的semid_ds数据结构选中的字段。
GETVAL:函数返回由semid指定的信号量集中第semnum个信号量的值,这个操做无需参数。
SETVAL:将有semid指定的信号量集中第semnum个信号量的值初始化为arg.val.
GETALL:获取由semid指向的信号量,集中全部信号量的值并将它们存放在arg.array指向的数组中。
SETALL:使用arg.array指向的数组中的值初始化semid指向的集合中的全部信号量。这个操做将忽略参数semnum。
每一个信号量集都有一个关联的semid_ds数据结构,其形式以下:
struct semid{ unsigned short sem_num;/*semaphore number*/ short sem_op; short sem_flg; }
函数semop()用于在semid标识的信号量集中的信号量上执行一个或多个操做,其函数原型以下:
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid,struct sembuf *sops,unsigned int nsops);
参数sops是一个指向数组的指针,函数中包含了须要执行的操做;参数nsops给出了数组的大小(数组中至少包含一个元素)。操做将会按照在数组中的顺序以原子的方式被执行。参数sops数组元素的结构形式以下:
struct sembuf{ unsigned short sem_num; short sem_op; short sem_flg; }
字段sem_num标识出了要操做的信号量;字段sem_op指定了要执行的操做:
若sem_op>0,这对应于进程释放占用的资源数,将sem_op的值加到信号量上,其结果是其余等待减少信号量值的进程可能会被唤醒并执行它们的操做。调用进程必需要具有在信号量上的修改权限。(V操做)
若sem_op<0,这表示要获取该信号量控制的资源数,将信号量减去sem_op。若是信号量当前的值大于或等于sem_op的绝对值,那么操做就会当即结束;不然函数semop()会阻塞直到信号量增加到在执行操做以后不会致使出现负值的状况为止。调用进程必需要具有在信号量上的修改权限。
若sem_op=0,这表示调用进程但愿等待到该信号量值变成0,那么就对信号量值进行检查以肯定它当前是否等于0.若是等于0,那么操做马上结束;不然函数semop()就会阻塞直到信号量值变为0为止。调用进程必需要具有在信号量上的读权限。
若是信号量值小于sem_op的绝对值(资源不能知足要求),则:
⑴若指定了IPC_NOWAIT,则semop()出错返回EAGAIN。
⑵若未指定IPC_NOWAIT,则信号量的semncnt值加1(由于调用进程将进入休眠状态),而后调用进程被挂起直至:①此信号量变成大于或等于sem_op的绝对值;②从系统中删除了此信号量,返回EIDRM;③进程捕捉到一个信号,并从信号处理程序返回,返回EINTR。
(与消息队列的阻塞处理方式 很类似)
从语义上讲,增长信号量值对应于使一种资源变得可用以便其余进程可使用它,而减小信号量值则对应于预留进程需使用的资源。在减少一个信号量值时,若是信号量的值过低——即其余一些进程已经预留了这个资源那么操做就会阻塞。
当函数semop()阻塞时,进程就会保持阻塞,直到发生下列状况为止:
(1)另外一个进程修改了信号量值使得待执行的操做可以继续向前。
(2)一个信号中断了semop()调用,这种状况下会返回错误码EINTR.
(3)另外一个进程删除了semid引用的信号量,这种状况下会返回错误码EIDRM。
示例:进程间通讯——读取信号量
sem_read.c源码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int main(int argc,char *argv[]) { int semid=0; int count=0; pid_t pd=0; struct sembuf sops; semid=semget((key_t)12345,3,0666|IPC_CREAT);//这里的0666是赋予读写权限 if(semid==-1) { perror("semget()"); exit(1); } printf("begin fork()\n"); for(count=0;count<3;count++) { pd=fork(); if(pd<0) { perror("fork()"); exit(1); } if(pd==0) { printf("child[%d]created!\n",getpid()); sops.sem_num=count; sops.sem_op=-1; sops.sem_flg=0; if(semop(semid,&sops,1)==-1) { perror("semop()"); exit(1); } printf("child[%d]exited!\n",getpid()); exit(0); } } exit(0); }
sem_write.c源码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int main(int argc,char *argv[]) { int semid=0; struct sembuf sops; if(argc!=2) { printf("sem_send usage error;\n"); exit(1); } semid=semget((key_t)12345,3,0666|IPC_CREAT); if(semid==-1) { perror("semget()"); exit(1); } if(strncmp(argv[1],"0",1)==0) { sops.sem_num=0; }else if(strncmp(argv[1],"1",1)==0) { sops.sem_num=1; }else if(strncmp(argv[1],"2",1)==0) { sops.sem_num=2; }else { perror("argument :count errro\n"); exit(1); } sops.sem_op=1; sops.sem_flg=SEM_UNDO; if(semop(semid,&sops,1)==-1) { perror("semop()"); exit(1); } else{ printf("semop(%d) over.\n",sops.sem_num); } exit(0); }
sem_write终端输入指令:
./sem_read终端运行结果
信号量的等待和挂出操做的函数分别是sem_wait 和sem_post,这两个函数的原型以下:
int sem_wait(sem_t sem); int sem_post(sem_t sem);
sem_wait 函数测试所指定信号量的值,若是该值大于0,则将它减1并当即返回。若是该值等于0.调用线程就会被投入到睡眠中,直到该值大于0,这时再将它减1,函数随后返回。这种测试信号量的值并进行减1的操做必须是原子的。
sem_post函数把所指定的信号量的值加1,而后唤醒正在等待改信号量变为整数的任意线程。当一个线程使用完某个信号量时,应该调用sem_post释放该信号量。
这两个函数调用成功后返回0,若出错返回-1.
另外还有两个函数有时也会被用到,分别是sem_trywait和sem_getvalue函数,这两个函数的原型以下:
int sem_trywait(sem_t* sem); int sem_getvalue(sem_t sem,*valp);
sem_trywait和sem_wait不一样的是,当信号量的值为0时,sem_trywait并不把调用进程投人睡眠。而是返回一个EAGAIN错误。
sem_getvalue获取信号量的值,存放在valp指向的地址中,若是该信号量当前已上锁,那么获取的值或为0,或者为某个负数,该负数的绝对值就是等待该信号量解锁的线程数。
基于名字的信号量使用sem_open、sem_close和sem_unlink函数来进行信号的建立、关闭和删除。
函数sem_open
sem_open用来建立一个信号量,函数原型以下:
sem_t* sem_open(const char* name,int oflag,.../*mode_t mode,unsigned int value*/);
参数name:信号量的名字,和消息队列建立的名字类似,须要以“/”开始。
参数oflag:打开仍是建立(以什么样的方式建立)。取值为0、O_CREATE或O_CREATE|O_EXECEL.若是取值为非0时,则须要指定下面两个参数。
参数mode:建立信号量时指定的权限为,以下表所示:
以上这些值定义在<sys/stat.h>中。
参数value:指定信号量的初始值,该初始值不能操做SEM_VALUE_MAX。一些Linux中,SEM_VALUE_MAX的值为32767。
须要注意的是,当以O_CREATE方式建立一个消息队列时,若是该消息队列已经存在,那么并不会返回一个错误,但不会用value去初始化该消息队列。若是该消息队列不存在,则建立,并用value初始化。若是以O_CREATE|O_EXECEL方式建立一个已经存在的消息队列,则会返回一个错误。
该函数成功则返回一个指向该信号量的指针,不然返回SEM_FAILED错误。
函数sem_close和sem_unlink
这两个函数和消息队列系列函数比较类似,其中sem_close只是仅仅关闭,不从物理上删除,若是须要从物理上删除则须要使用sem_unlink。原型以下:
int sem_close(sem_t* sem); int sem_unlink(const char* name);
成功返回0,不然返回-1.
基于内存的信号量使用sem_init、sem_destroy函数来进行信号的建立和销毁,这两种函数原型以下:
int sem_init(sem_t* sem, int shared,unsigned int value); int sem_destory(sem* sem);
这两个函数都是成功则返回0,不然返回-1。sem_destroy的参数就是sem_init建立的信号量的指针。sem_init中参数sem是一个指针,用于存放信号量,必须由应用程序来分配。参数shared表示是否共享,若是为0表示不共享,则只能在同一进程的不一样线程之间使用;若是不为0则表示共享,则该信号量能够在不一样进程之间共享,但前提是保存信号量的内存是各进程共享的(后面会讲到共享内存)。
在这里再次说明,基于名字的信号量具备自然的进程间共享属性,因此并不须要像基于内存的信号量这样指定共享属性。
共享内存是进程间通讯的最快方式。不管使用管道、消息队列仍是socket等手段,这些方法都须要使用诸如read、write等系统调用,可是用共享内存却没必要使用系统调用。共享内存能够是将实际存在的物理文件映射到一段内存中,也能够将一个POSIX内存区对象映射到一段内存地址中。共享内存在访问时每每须要使用一些信号量进行同步。
共享内存才能这种IPC机制不禁内核控制,意味着一般须要经过某种同步方法使得进程不会出现同时访问共享内存的状况(如两个进程同时执行更新操做或者一个进程在从共享内存中获取数据的同时另外一个进程正在更新这些数据)。信号量就是用来完成这种同步的一种方法。
使用共享内存一般须要遵循下述步骤:
(1)调用函数shmget()建立一个新共享内存段或取得一个既有共享内存段的标识符。
(2)调用函数shmat()附上共享内存段,即便该段是调用进程的虚拟内存的一部分。
(3)此刻在程序中能够像对待其余可用内存那样对待这个共享内存段。为引用这块共享内存,程序须要使用由shmat()调用返回的addr值,它是一个指向进程的虚拟地址空间中该共享内存段起点的指针。
(4)调用函数shmdt()分离共享内存段。调用以后,进程没法再引用这段共享内存。这一步是可选的,而且在进程终止时会自动完成这一步。
(5)调用函数shmctl()删除共享内存段。只有当目前全部附加内存段的进程都与之分离后,内存段才能被销毁。只有一个进程须要执行这一步。
函数shmget()用于建立一个新的共享内存段或获取一个既有段的标识符,新建立的共享内存段的内容会被初始化为0,其函数原型以下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key,size_t size,int shmflg);
参数key用于指定共享内存段的名称,其特殊值IPC_PRIVATE的做用是建立一个仅能由本进程访问的私用共享内存段;参数size用于指定共享内存端分配所需的字节数,内核是以系统分页大小的整数倍来分配共享内存的,所以实际上size会被提高到最近的系列分页大小的整数倍;参数shmflg执行的任务与在IPC get调用中执行的任务同样,即指定施加于新共享内存段上的权限或须要检查的既有内存段的权限。
函数shmat()将共享内存端附加到调用进程的虚拟地址空间中,其函数原型以下:
#include <sys/ipc.h> #include <sys/shm.h> void *shmat(int shmid,const void *shamaddr,int shmflg);
参数shmaddr和shmflg(位掩码SHM_RND)的设置控制着共享内存段是如何被附加上去的:
(1)若是shmaddr=NULL,那么共享内存段附加到内核所选择的一个合适的地址处,这是最优选择的方法。
(2)若是shmaddr不是NULL而且没有设置SHM_RND,那么段会附加到由shmaddr指定的位置处,它必须是系统分页得一个倍数(不然会发生EINVAL错误)。
(3)若是shmaddr不是NULL而且设置了SHM_RND,而且设置了SHMLBA,那么段会被映射到shmaddr提供的地址,同时将地址设置为SHMLBA的倍数,这个常量等于系统分页大小的某个倍数。将一个段附加到值为SHMLBA的倍数的地址处,这在一些架构上是有必要的,由于这样才可以提高CPU的快速缓冲性能和防止出现同一个段的不一样附加操做在CPU快速缓冲区中存在不一致的视图状况。
不推荐shmaddr指定一个非NULL值,缘由以下:
(1)它下降了一个应用程序的可移植性。一个在Linux系统上立刻有效的地址,在另外一个系统上可能无效。
(2)试图将一个共享内存段附加到一个正在使用的特定地址处的操做会失败。例如,当一个应用程序已经在该地址出附加了另外一个段或建立要给内存映射时,就会发生这种状况。
函数shmat()返回的结果是附加共享内存段的地址,开发人员能够像对待普通的C指针那样对待这个值,段与进程的虚拟内存的其余部分毫无差别。一般会将函数shmat()返回值赋给一个由程序员定义的结构指针,以便在该段上设定该结构。
要附加一个共享内存段以供只读访问,那么就须要在参数shmflg中指定SHM_RONLY标记。试图更新之毒段中的内容会致使段错误(SIGSEGV信号)的发生。若是没有指定SHM_RDONLY,那么能够读取内存又能够修改内存。
一个进程要附加一个共享内存段须要在该段上具有读和写的权限,除非指定了SHM_RDONLY标记——这样的话就只需具有读权限便可。
一个进程再也不须要访问一个共享内存段时,能够调用函数shmdt()将该段分离出器虚拟地址空间,函数原型以下:
#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr);
参数shmaddr标识出待分离的段,他是以前调用函数shmat()返回的一个值。经过fork()建立的子进程会继承其父进程附加的共享内存段,所以,共享内存为父进程和子进程之间的通讯提供了一种简单的IPC方法。
shmctl 与信号量的semctl()函数同样,用来控制共享内存,它的原型以下:
#include <sys/types.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf)
第一个参数,shm_id是shmget()函数返回的共享内存标识符。
第二个参数,command是要采起的操做,它能够取下面的三个值 :
第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。
shmid_ds结构 至少包括如下成员:
struct shmid_ds { uid_t shm_perm.uid; uid_t shm_perm.gid; mode_t shm_perm.mode; };
示例:经过共享内存进行进程间的通讯
shm_read.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <sys/stat.h> int main(int argc,char *argv) { int fd=0; int shmid=0; char *buf; shmid=shmget((key_t)12345,4096,0666|IPC_CREAT); if(shmid<0) { perror("shget()"); exit(1); } buf=(char*)shmat(shmid,NULL,0); if(buf==(void*)-1) { perror("shmat()"); exit(1); } if(strcmp(buf,"")==0) { printf("read nothing\n"); } else( printf("read:%s\n",buf); } if(shmdt(buf)==-1) { perror("shmdt()"); exit(1); } }
shm_write.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/ipc.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/shm.h> int main(int argc,char *argv) { int fd=0; int shmid=0; char *buf; fd=open("test.txt",O_RDONLY); if(fd<0) { perror("open()"); exit(1); } shmid=shmget((key_t)12345,4096,0666|IPC_CREAT); if(shmid<0) { perror("shmget()"); exit(1); } buf=(char*)shmat(shmid,NULL,0); if(buf==(void*)-1) { perror("shmat()"); exit(1); } if(read(fd,buf,1024)==-1) { perror("read()"); exit(1); }else { printf("write successful.\n"); } if(shmdt(buf)==-1) { perror("shmdt()"); exit(1); } }
程序运行结果以下:
内存映射大体分为两种。
(1)文件映射:文件映射将一个文件的一部分直接映射到进程的虚拟内存中。一旦一个文件被映射以后就能够经过在相应的内存区域操做字节来访问文件内容,映射的分页会在须要的时候从文件中(自动)加载,这种映射也被称为基于文件的映射或内存映射文件。
(2)匿名映射:一个匿名映射没有对应的文件,这种映射的分页会被初始化为0.
一个进程的映射中的内存能够与其余进程中的映射共享(即各个进程的页表条目指向RAM中的相同分页),这会在两种状况下发生:
(1)当两个进程映射了一个文件的同一个区域时,他们会共享物理内存的相同分页。
(2)经过函数fork()建立的子进程会继承其父进程的映射的副本,而且这些映射所引用的物理内存分页与父进程中相应映射所引用的分页相同。
当两个或多个进程共享相同分页时,每一个进程都有可能会看到其余进程对分页内容做出的变动,固然这要取决于映射是私有的仍是共享的。
私有映射(MAP_PRIVATE):在映射内容上发生的变动对其余进程不可见,对于文件映射来说,变动将不会在底层文件上进行。
共享映射(MAP_SHARE):在映射内容上发生的变动对全部共享同一个映射的其余进程均可见,对于文件映射来说,变动将会发生在底层文件上。
mmapz把一个文件或者一个POSIX内存对象区映射到调用进程的地址空间,用于 在调用进程的虚拟地址空间上建立一个新映射,该函数的原型以下:
#include <sys/mman.h> void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);
函数执行成功,返回新映射的起始地址;发生错误,返回MAP_FAILED。
参数addr指定了映射放置的虚拟地址;若是addr指定为NULL,那么内核会映射选择一个合适的地址,若是add为非NULL,内核会在选择映射放置在何处时将这个参数值做为一个提示信息来处理。
参数length指定了映射的字节数,length无须是一个系统分页大小的倍数,但内核会之内存页大小为单位来建立映射,所以实际上length会被向上提高为分页大小的下一个倍数。
参数port是一个位掩码,它指定了施加于映射之上的保护信息,其取值为:
PROT_NOT:区域没法访问。
PROT_READ:区域内容可读取。
PROT_WRITE:区域内容可修改。
PROT_EXEC:区域内容可执行。
参数flags是一个控制映射操做各个方面选项的位掩码,这个掩码只能是下列值之一:
MAP_PRIVATE:建立一个私有映射,区域内容上发生的改变对使用同一内存的其余进程是不可见的。对于文件映射来说,所发生的变动将不会反映在底层文件上。
MAP_SHARED:建立一个共享映射,区域内容上所发生的变动对使用MAP_SHARED特性映射同一区域的进程是可见。对文件映射来说,所发生的变动将直接反映在底层文件上。
剩余的参数fd和offset是用于文件映射的。参数fd是一个被映射文件的描述符;参数offset指定了映射在文件中的起点,他必须是系统分页大小的倍数,要映射整个文件就须要将offset指定为0,而且将length指定为文件大小。
函数munmap()执行的操做与mmap()相反,即在调用进程的虚拟地址空间中删除一个映射,其函数原型以下:
#include <sys/munmap.h> int munmap(void *addr,size_t length);
参数addr:是由mmap返回的映射区起始地址。
参数length:映射区的大小。
函数成功返回0,不然返回-1.
msync函数是强制同步映射区与底层文件一致。若是mmap是以MAP_SHARED方式映射,那么对于映射去的修改会被内核更新到磁盘文件,但并不必定老是马上更新。有时候可能须要确信硬盘文件的内容和映射区的内容是否一致,便可以使用该函数进行同步。该函数的原型以下:
void* msync(void* addr,size_t len,int flags);
参数addr:是由mmap返回的映射区起始地址。
参数len:映射区大小,一般是整个映射区的大小,但也能够只是一部分。
参数flags:表示更新的方式,取值以下:
MS_ASYNC:执行异步写。
MS_SYNC:执行同步写。
MS_INVALIDATE:使高速缓存的数据失效数据可执行。
在取值中,MS_ANYNC和MS_SYNC必须指定一个,但不能同时指定。若是制定MS_ASYNC,则须要等到写操做完成,即写入到磁盘后才会返回。若是同时还指定了MS_INVALIDATE,则与其最终副本不一致的文件数据的全部内存中副本都失效,后续的引用将从文件中取得数据。
示例:内存映射方式进程间通讯
mmap_write.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <sys/mman.h> #include <sys/stat.h> #include <sys/types.h> typedef struct { char name[4]; int age; }people; int main(int argc,char *argv[]) { int fd=0; int count=0; people *p_map; char temp='a'; fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,0666); if(fd<0) { perror("open()"); exit(1); } lseek(fd,sizeof(people)*10-1,SEEK_SET); if(write(fd,"",1)<0) { perror("write()"); exit(1); } p_map=(people*)mmap(NULL,10*sizeof(people),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); if(p_map==(void*)-1) { perror("mmap()"); exit(1); } close(fd); for(count=0;count<10;count++) { temp+=1; memcpy((*(p_map+count)).name,&temp,2); (*(p_map+count)).age=20+count; } printf("mmap write finished.\n"); exit(0); }
mmap_read.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/mman.h> typedef struct { char name[4]; int age; }people; int main(int argc,char *argv[]) { int fd=0; int count=0; people *p_map; fd=open(argv[1],O_CREAT|O_RDWR,0666); if(fd<0) { perror("open()"); exit(1); } p_map=(people*)mmap(NULL,10*sizeof(people),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); if(p_map==(void*)-1) { perror("mmap()"); exit(1); } for(count;count<10;count++) { printf("name:%s age:%d\n",(*(p_map+count)).name,(*(p_map+count)).age); } munmap(p_map,10*sizeof(people)); exit(0); }
程序运行结果以下:
处理将文件映射到共享内存外,POSIX还提供了一种将内存对象映射到共享内存的方法。POSIX内存区对象就是使用shm_open打开一个POSIX的名字,和基于名字的信号量打开相似。可是这种映射的地步支撑对象不必定是物理文件。可能就是在内存中。
shm_open函数用来打开或者建立一个POSIX内存区对象,对于mmap函数而言,shm_open与open函数打开一个文件没有什么区别,只是shm_open函数是在/dev/shm目录上生成一个文件,并且会校验该目录下是否是挂载了tmpfs文件系统,若是不是也不能正常打开的。因此通常仍是用shm_open函数更规范一些,由于这个文件存在tmpfs文件系统下,在不用的状况系统会自动删除掉。该函数的原型以下:
int shm_open(const char* name,int oflag,mode_t mode);
参数name:是内存对象的名字,必须以“/”开始。
参数oflag:打开或者建立方式。oflag必须含有O_RDONLY或O_RDWR标准,还能够制定O_CREAT、O_EXCL或O_TRUNC。O_CREAT表示建立,O_EXCL表示排他性建立。若是指定O_TRUNC,则若是该内存对象存在,那么它将被截短成为0长度。
参数mode:表示权限。若是须要赋予权限位。
shm_unlink用来删除一个POSIX内存区对象,该函数的原型以下:
int shm_unlink(const char* name);
参数name:使用shm_open打开的POSIX内存去对象名字。
该函数成功返回0,失败返回-1.
须要注意的是,shm_unlink仅仅只是删除一个名字,防止其余shm_open再次打开而已,其底层的支撑对象并不会被删除。直到全部的对于该对象的应用关闭后,该对象将被删除。
示例:
shm_open_w.c
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<fcntl.h> #include<sys/types.h> #include<sys/stat.h> #include<sys/mman.h> /*封装打印出错函数*/ void sys_err(const char *str,int num){ perror(str); exit(num); } int main(int argc,char *argv[]) { int fd = shm_open("/hello.txt",O_RDWR|O_CREAT|O_EXCL,0777); /*O_EXCL|O_CREAT,若文件已经存在,则报错*/ if(fd < 0){ fd = shm_open("/hello.txt",O_RDWR,0777); /*直接打开文件读写*/ }else ftruncate(fd,4096); /*若为本身建立的文件,则为文件分配内存空间大小*/ void *ptr = mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); puts("start writeing data...."); /*这里为写文件*/ strcpy((char*)ptr,"Hello\n"); puts("write over"); getchar(); shm_unlink("/hello.txt"); close(fd); return 0; }
shm_open_r.c
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<fcntl.h> #include<sys/types.h> #include<sys/stat.h> #include<sys/mman.h> void sys_err(const char *str,int num){ perror(str); exit(num); } int main(int argc,char *argv[]) { int fd = shm_open("/hello.txt",O_RDWR|O_CREAT|O_EXCL,0777); if(fd < 0){ fd = shm_open("/hello.txt",O_RDWR,0777); }else ftruncate(fd,4096); void *ptr = mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); puts("start reading data...."); puts((char*)ptr); puts("read over"); shm_unlink("/hello.txt"); close(fd); return 0; }
编译运行结果以下: