进程间通信(Interprocess Communication,IPC)是一个描述两个进程彼此交换信息的通用术语。一般情况下,通信的两个进程即可以运行在同一台机器上,也可以运行在不同的机器上。进程间的通信是数据的交换,两个或多个进程合作处理数据或同步信息,以帮助两个彼此独立但相关联的进程调度工作,避免重复工作。进程间通信方式有很多种,比如可以使用socket、使用管道、消息队列、文件、共享内存等。
1、管道
管道是进程间通信中最古老的方式,他使得数据以一种数据流的方式在多个进程之间流动。管道相当于文件系统上的一个文件,用来缓存所要传输的数据,但是在某些特性上又不同于文件,例如,当数据读出后,管道中的数据就没有了, 单文件就没有这个特性。综合来说,管道具有以下特点:
1) 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
2) 匿名管道只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
3) 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
管道分为pipe(无名管道)和fifo(命名管道)两种,除了建立、打开、删除的方式不同外,这两种管道几乎是一样的。他们都是通过内核缓冲区实现数据传输。前者用于父进程和子进程间的通信,后者用于运行于同一台机器上的任意两个进程间的通信
- pipe用于相关进程之间的通信,例如父进程和子进程,它通过pipe()系统调用来创建并打开,当最后一个使用它的进程关闭对他的引用时,pipe将自动撤销。
- FIFO即命名管道,在磁盘上有对应的节点,但没有数据块——换言之,只是拥有一个名字和相应的访问权限,通过mknode()系统调用或者mkfifo()函数来建立的。一旦建立,任何进程都可以通过文件名将其打开和进行读写,而不局限于父子进程,当然前提是进程对FIFO有适当的访问权。当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。
pipe(无名管道)
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]的数据通道。
要关闭管道只需将这两个文件描述符关闭即可。
单独操作一个进程管道是没有意义的,管道的应用一般体现在父子进程或者兄弟进程之间的通信上。如果要建立一个父进程到子进程的数据通道,需要先调用函数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(有名管道)
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可得到如下运行结果
2、信号
信号是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
函数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
函数sigaction与函数signal功能类似,主要用于定义在接收到信号后应该采取的处理方式,其函数原型如下:
#include <signal.h> int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存返回的原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为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; }
其中,结构体的关键成员含义
1、联合数据结构中的两个元素_sa_handler以及*_sa_sigaction指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。
2、由_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时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可以让信号传递一些附加信息。信号可以传递信息对程序开发是非常有意义的。
3、sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。
注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。
4、sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)。
函数kill
函数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
函数raise()用于进程向自身发送信号,其函数原型如下:
#include <signal.h> int raise(int signo)
参数signo为即将发送的信号值。调用成功返回 0;否则,返回 -1。
函数pause
函数pause()用于将调用进程挂起直至捕捉到信号为止,通常用于判断信号是否到达,其函数原型如下:
#include <unistd.h> int pause(void);
函数sigqueue
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
函数alarm()也称为闹钟函数,是专门为信号SIGALARM而设的,用于在指定的时间项进程本身发送SIGALARM信号,其函数原型如下:
#include <unistd.h> unsigned int alarm(unsigned int seconds)
如果指定的参数seconds为0,则不再发送 SIGALRM信号。后一次设定将取消前一次的设定。该调用返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回0。
函数setitimer
现在的系统中很多程序不再使用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 (;;) ; }
程序运行结果如下:
函数abort
向进程发送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
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函数就是在捕捉一个信号或发生了一个会终止该进程的信号之前,将进程投入睡眠,直到该信号来到并从信号处理函数中返回。sigsuspend函数原型如下:
int sigsuspend(const sigset_t *mask));
参数sigmask,将进程的信号屏蔽字设置为sigmask,也就是说进程会在睡眠后的信号屏蔽字。
因此在使用sigsuspend函数时,当该函数返回后,应该将进程原来的屏蔽字再重新设置回去。
函数sigpending
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; }
信号集运行结果如图
3、消息队列
消息队列是消息的链接表,存放在内核中并由消息队列标识符标识。消息队列与FIFO有许多相似之处,但是少了管道打开文件和关闭文件的麻烦。它可用于不同进程间的通信,但是其重点还是线程之间的一种通信方式。现在首先详细对进程间的通信进行讲解。
1、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.
2、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.
3、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。
4、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个值,
- IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
- IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值
- IPC_RMID:删除消息队列
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); }
程序编译运行结果如下:
4、信号量
信号量是进程间通信之前进行进程间同步必须要掌握的内容,信号量(Semaphore)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语,以防止一个进程在访问共享内存的同时拎一个进程更新这块内存的情况。POSIX有两种信号量:一种是基于名字的信号量,一种是基于内存的信号量。这两种信号量都可以用于不同进程或者不同线程之间的同步。
基于名字的信号量,是一种天然的适用于不同进程的同步。因为这种所谓的名字和文件系统进行了关联,这样就使得各进程都可以访问(其实未必真正的和实际的文件相关联,只不过对于应用来讲,用起来就像文件一样)。
基于内存的信号量,如果放置在进程间的共享内存中,既可以进行进程间的同步。尽管这两种信号两都可以用于进程或者线程之间的同步,但原则上基于名字的信号量更多的应用于不同进程间的同步,而基于内存的信号量更多的用于线程间的同步。
为了获得共享资源,进程需要执行下列操作:
(1)测试控制该资源的信号量。
(2)若信号量的值为正,则进程可以使用该资源,这种情况下,进程会将信号量的值减1,表示它使用了一个资源单位。
(3)若信号量的值为负,则进程进入休眠状态,直至信号量值大于0;进程被唤醒后,返回步骤1.
(4)当进程不再使用由一个信号量控制的共享资源时,该信号量增1;如果有进程正在休眠等待此信号,则唤醒它们。
为了正确实现信号量,信号量值的测试及减1操作应当是原子操作,为此,信号量通常是在内核中实现的。
常用的信号量形式被称为二元信号量,它控制单个资源,其初始值为1.但是一般而言,信号量的初值可以是任意一个正值,改值表明有多少个共享资源单位可共享应用。
信号量的等待和挂出:
函数semget
函数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
函数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
函数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终端运行结果
4、进程间同步
函数set_wait和sem_post
信号量的等待和挂出操作的函数分别是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则表示共享,则该信号量可以在不同进程之间共享,但前提是保存信号量的内存是各进程共享的(后面会讲到共享内存)。
在这里再次说明,基于名字的信号量具有天然的进程间共享属性,所以并不需要像基于内存的信号量这样指定共享属性。
5、共享内存
共享内存是进程间通信的最快方式。无论使用管道、消息队列还是socket等手段,这些方法都需要使用诸如read、write等系统调用,但是用共享内存却不必使用系统调用。共享内存可以是将实际存在的物理文件映射到一段内存中,也可以将一个POSIX内存区对象映射到一段内存地址中。共享内存在访问时往往需要使用一些信号量进行同步。
共享内存才能这种IPC机制不由内核控制,意味着通常需要通过某种同步方法使得进程不会出现同时访问共享内存的情况(如两个进程同时执行更新操作或者一个进程在从共享内存中获取数据的同时另一个进程正在更新这些数据)。信号量就是用来完成这种同步的一种方法。
使用共享内存通常需要遵循下述步骤:
(1)调用函数shmget()创建一个新共享内存段或取得一个既有共享内存段的标识符。
(2)调用函数shmat()附上共享内存段,即使该段是调用进程的虚拟内存的一部分。
(3)此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由shmat()调用返回的addr值,它是一个指向进程的虚拟地址空间中该共享内存段起点的指针。
(4)调用函数shmdt()分离共享内存段。调用之后,进程无法再引用这段共享内存。这一步是可选的,并且在进程终止时会自动完成这一步。
(5)调用函数shmctl()删除共享内存段。只有当目前所有附加内存段的进程都与之分离后,内存段才能被销毁。只有一个进程需要执行这一步。
函数shmget
函数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 -- at:attach
函数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-- dt:detach
一个进程不再需要访问一个共享内存段时,可以调用函数shmdt()将该段分离出器虚拟地址空间,函数原型如下:
#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr);
参数shmaddr标识出待分离的段,他是之前调用函数shmat()返回的一个值。通过fork()创建的子进程会继承其父进程附加的共享内存段,因此,共享内存为父进程和子进程之间的通信提供了一种简单的IPC方法。
函数shmctl -- ctl:control
shmctl 与信号量的semctl()函数一样,用来控制共享内存,它的原型如下:
#include <sys/types.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf)
第一个参数,shm_id是shmget()函数返回的共享内存标识符。
第二个参数,command是要采取的操作,它可以取下面的三个值 :
- IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
- IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
- IPC_RMID:删除共享内存段
第三个参数,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):在映射内容上发生的变更对所有共享同一个映射的其他进程都可见,对于文件映射来讲,变更将会发生在底层文件上。
函数mmap
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()
函数munmap()执行的操作与mmap()相反,即在调用进程的虚拟地址空间中删除一个映射,其函数原型如下:
#include <sys/munmap.h> int munmap(void *addr,size_t length);
参数addr:是由mmap返回的映射区起始地址。
参数length:映射区的大小。
函数成功返回0,否则返回-1.
函数msync
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还提供了一种将内存对象映射到共享内存的方法。POSIX内存区对象就是使用shm_open打开一个POSIX的名字,和基于名字的信号量打开类似。但是这种映射的地步支撑对象不一定是物理文件。可能就是在内存中。
函数shm_open
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
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; }
编译运行结果如下:
参考资料
进程间通信的方式——信号、管道、消息队列、共享内存
进程间通信
Linux C 程序设计大全—进程间通信
Linux 信号详解一(signal函数)
Linux信号(signal) 机制分析
Linux信号 - 示例C捕获信号的程序(SIGINT,SIGKILL,SIGSTOP等)
2.信号
System V 信号量
linux信号量(转载)
Linux进程间通信(五):信号量 semget()、semop()、semctl()
linux基础——linux进程间通信(IPC)机制总结
Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl()
Linux进程间通信(七):消息队列 msgget()、msgsend()、msgrcv()、msgctl()
msgget();msgsnd();msgrcv();msgctl(); 消息队列 Linux进程间的通信方式之消息队列
linux c学习笔记----消息队列(ftok,msgget,msgsnd,msgrcv,msgctl)
linux应用编程笔记(14)共享内存编程
共享内存
共享内存函数(shmget、shmat、shmdt、shmctl)及其范例
mmap和shm共享内存的区别和联系
linux 共享内存shm_open实现进程间大数据交互
程序报错 undefined reference to `shm_open'
-lz -lrt -lm -lc都是什么库