学习内容:在前面的学习中,咱们学习了如何建立进程以及如何对进程进行基本的控制,而这些都只是停留在父子进程之间的控制,本次将要学习不一样的进程间进行通讯的方法。html
在以前的学习中咱们了解到,进程是一个程序的一次执行。这里所说的进程通常是指运行在用户态的进程,而因为处于用户态的不一样进程之间是彼此隔离的,它们必须经过某种方式来进行通讯,就像现在不一样地域的人们使用手机联系同样。接下来咱们将学习如何为不一样的进程间创建通讯方式。node
Linux下的进程通讯手段基本上是从UNIX 平台上的进程通讯手段继承而来的。而对UNIX发展作出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间的通讯方面的重点有所不一样。前者是对UNIX早期的进程间通讯手段进行了系统的改进和扩充,造成了“system V IPC”,其通讯进程主要局限在单个计算机内;后者则跳过了该限制,造成了基于套接口(socket)的进程间通讯机制。而Linux则把二者的优点都继承了下来,以下图所示:linux
如今Linux中经常使用的进程间通讯方式主要有如下几种:程序员
接下来将详细介绍前5种进程间通讯方式,套接字在“网络编程”部分重点研究。编程
『1.管道概述』数组
管道是Linux中一种很重要的通讯方式,它能够把一个程序的输出直接链接到另外一个程序的输入。还记得咱们以前使用man -k process | grep create
命令搜索与建立进程相关的函数:网络
这就是管道的一种使用方式,即把man -k process
命令的输出当作grep create
命令的输入,进行二次检索。数据结构
管道是Linux中进程间通讯的一种方式。这里所说的管道主要指无名管道,它具备如下特色:异步
『2.管道系统调用』socket
(1)管道建立与关闭说明:
管道是基于文件述符的通讯方式,当一个管道创建时,它会建立两个文件述符fds[0]和fds[1],其中fds[0]固定用于读管道,而fd[1]固定用于写管道,这样就构成了一个半双工的通道。
管道关闭时只需将这两个文件描述符关闭便可,可以使用普通的close()函数逐个关闭各个文件描述符。
注意:当一个管道共享多对文件描述符时,若将其中一对读写文件描述符都删除,则该管道就失效。
(2)管道建立函数:
建立管道能够经过调用pipe()实现,如下列出了pipe()函数的语法要点:
(3)管道读写说明:
通常状况下使用管道时,先建立一个管道,再经过fork()函数建立一子进程,该子进程会继承父进程所建立的管道。为了实现父子进程之间的读写,只需把无关的读端或写端的文件描述符关闭便可。例如在下图中将父进程的写端fd[1]和子进程的读端fd[0]关闭。此时,父子进程之间就创建起了一条“子进程写入父进程读取”的通道。
一样,也能够关闭父进程的fd[0]和子进程的fd[1],这样就能够创建一条“父进程写入子进程读取”的通道。另外,父进程还能够建立多个子进程,各个子进程都继承了相应的fd[0]和fd[1],这时,只须要关闭相应端口就能够创建其各子进程之间的通道。
(4)管道使用实例:
在下面的测试代码中,首先建立管道,以后父进程使用fork()函数建立子进程,以后经过关闭父进程的读描述符和子进程的写描述符,创建起父子进程之间的管道通讯。
/* pipe.c */ #include <unistd.h> #include <sys/types.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #define MAX_DATA_LEN 256 #define DELAY_TIME 1 int main() { pid_t pid; int pipe_fd[2]; char buf[MAX_DATA_LEN]; const char data[] = "Pipe Test Program"; int real_read, real_write; memset((void*)buf, 0, sizeof(buf)); /* 建立管道 */ if (pipe(pipe_fd) < 0) { printf("pipe create error\n"); exit(1); } /* 建立一子进程 */ if ((pid = fork()) == 0) { /* 子进程关闭写描述符,并经过使子进程暂停1s等待父进程关闭相应的读描述符*/ close(pipe_fd[1]); sleep(DELAY_TIME * 3); /* 子进程读取管道内容 */ if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0) { printf("%d bytes read from the pipe is '%s'\n", real_read, buf); } /* 关闭子进程读描述符 */ close(pipe_fd[0]); exit(0); } else if (pid > 0) { /* 父进程关闭读描述符,并经过使父进程暂停1s等待子进程关闭相应的写描述符*/ close(pipe_fd[0]); sleep(DELAY_TIME); if((real_write = write(pipe_fd[1], data, strlen(data))) != -1) { printf("Parent wrote %d bytes : '%s'\n", real_write, data); } /* 关闭父进程写描述符 */ close(pipe_fd[1]); /* 收集子进程退出信息 */ waitpid(pid, NULL, 0); exit(0); } }
运行结果以下图所示:
『3.标准流管道』
(1)标准流管道函数说明:
与Linux的文件操做中有基于文件流的标准I/O操做同样,管道的操做也支持基于文件流的模式。这种基于文件流的管道主要是用来建立一个链接到另外一个进程的管道,这里的“另外一个进程”也就是一个能够进行必定操做的可执行文件,例如,用户执行“ls -l”或者本身编写的程序“./pipe”等。因为这一类操做很经常使用,所以标准流管道就将一系列的建立过程合并到一个函数popen()中完成。它所完成的工做有如下几步。
这个函数的使用能够大大减小代码的编写量,但同时也有一些不利之处,例如,它不如前面管道建立的函数那样灵活多样,而且用popen()建立的管道必须使用标准I/O函数进行操做,但不能使用前面的read()、write()一类不带缓冲的I/O函数。
与之相对应,关闭用popen()建立的流管道必须使用函数pclose()来关闭该管道流。该函数关闭标准I/O流,并等待命令执行结束。
(2)函数格式:
(3)函数使用实例:
下面的程序使用popen()来执行“ps -ef”命令,能够看出popen()函数使程序变得短小精悍:
/* standard_pipe.c */ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #define BUFSIZE 1024 int main() { FILE *fp; char *cmd = "ps -ef"; char buf[BUFSIZE]; if ((fp = popen(cmd, "r")) == NULL) { printf("Popen error\n"); exit(1); } while ((fgets(buf, BUFSIZE, fp)) != NULL) { printf("%s",buf); } pclose(fp); exit(0); }
运行结果以下:
『4.FIFO』
(1)有名管道说明
前面介绍的管道是无名管道,它只能用于具备亲缘关系的进程之间,这就大大地限制了管道的使用。有名管道的出现突破了这种限制,它可使互不相关的两个进程实现彼此通讯。该管道能够经过路径名来指出,而且在文件系统中是可见的。在创建了管道以后,两个进程就能够把它看成普通文件同样进行读写操做,使用很是方便。不过值得注意的是,FIFO是严格地遵循先进先出规则的,对管道及FIFO的读老是从开始处返回数据,对它们的写则把数据添加到末尾,它们不支持如lseek()等文件定位操做。
有名管道的建立可使用函数mkfifo(),该函数相似文件中的open()操做,能够指定管道的路径和打开的模式。
在建立管道成功以后,就可使用open()、read()和write()这些函数了。与普通文件的开发设置同样,对于为读而打开的管道可在open()中设置O_RDONLY,对于为写而打开的管道可在open()中设置O_WRONLY,在这里与普通文件不一样的是阻塞问题。因为普通文件的读写时不会出现阻塞问题,而在管道的读写中却有阻塞的可能,这里的非阻塞标志能够在open()函数中设定为O_NONBLOCK。
(2)mkfifo()函数格式
(3)使用实例
下面的实例包含了两个程序,一个用于读管道,另外一个用于写管道。其中在读管道的程序里建立管道,而且做为main()函数里的参数由用户输入要写入的内容。读管道的程序会读出用户写入到管道的内容,这两个程序采用的是阻塞式读写管道模式。
/* fifo_write.c 写管道*/ #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <limits.h> #define MYFIFO "/tmp/myfifo" //有名管道文件名 #define MAX_BUFFER_SIZE PIPE_BUF int main(int argc, char * argv[]) //参数为即将写入的字符串 { int fd; char buff[MAX_BUFFER_SIZE]; int nwrite; if(argc <= 1) { printf("Usage: ./fifo_write string\n"); exit(1); } sscanf(argv[1], "%s", buff); /*以只写阻塞方式打开FIFO管道*/ fd = open(MYFIFO, O_WRONLY); if (fd == -1) { printf("Open fifo file error\n"); exit(1); } /*向管道中写入字符串*/ if ((nwrite = write(fd, buff, MAX_BUFFER_SIZE)) > 0) { printf("Write '%s' to FIFO\n", buff); } close(fd); exit(0); }
/*fifo_read.c 读管道程序*/ #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <limits.h> #define MYFIFO "/tmp/myfifo" #define MAX_BUFFER_SIZE PIPE_BUF int main() { char buff[MAX_BUFFER_SIZE]; int fd; int nread; /*判断有名管道是否已存在,若还没有建立,则以相应的权限建立*/ if (access(MYFIFO, F_OK) == -1) { if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST)) { printf("Cannot create fifo file\n"); exit(1); } } /*以只读阻塞方式打开有名管道*/ fd = open(MYFIFO, O_RDONLY); if (fd == -1) { printf("Open fifo file error\n"); exit(1); } while (1) { memset(buff, 0, sizeof(buff)); if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0) { printf("Read '%s' from FIFO\n", buff); } } close(fd); exit(0); }
为了可以较好地观运行结果,须要把这两个程序分别在两个终端里运行,在这里首先启动读管道程序。读管道进程在创建管道以后就开始循环地从管道里读出内容,若是没有数据可读,则一直到写管道进程向管道写入数据。在启动了写管道程序后,读进程可以从管道里读出用户的入内容,程序运行结果以下所示:
终端一:
终端二:
『1.信号概述』
信号是UNIX中所使用的进程通讯的一种最古老的方法。它是在软件层次上对中断机制的一种模拟,是一种异步通讯方式。
使用kill -l
命令能够列出该系统所支持的全部信号的列表:
在上图所示系统中,,信号值在32 以前的则有不一样的名称,而信号值在32 之后的都是用“SIGRTMIN”或“SIGRTMAX”开头的,这就是两类..型的信号。前者是从UNIX 系统中继承下来的信号,为不可靠信号(也称为非实时信号);后者是为了解决前面“不可靠信号”的问题而进行了更改和扩充的信号,称为“可靠信号”(也称为实时信号)。
那么,为何以前的信号不可靠呢?这里咱们首先介绍一下信号的生命周期。
一个完整的信号生命周期能够分为3个重要阶段,这3个阶段由4个重要事件来刻画:信号产生、信号在进程中注册、信号在进程中注销、执行信号处理函数,相邻两个事件的时间间隔构成信号生命周期的一个阶段,以下图所示:
信号处理有多种方式,通常是由内核完成的,固然也能够由用户进程来完成,故在此没有明确画出。
用户进程对信号的响应能够有3种方式:
Linux中的大多数信号是提供给内核的,使用man 7 signal
能够查看信号的定义及其默认操做。下图展现了一部分:
『2.信号的发送与捕捉』
如今咱们想了解与信号有关的函数都有哪些,使用man -k signal
可查询到全部与signal有关的函数,咱们重点研究如下几个:kill()、raise()、alarm()以及pause()。
(1)kill()和raise()
kill()函数同读者熟知的kill系统命令同样,能够发送信号给进程或进程组(实际上,kill系统命令只是kill()函数的一个用户接口)。这里须要注意的是,它不只能够停止进程(实际上发出SIGKILL信号),也能够向进程发其余信号。
与kill()函数所不一样的是,raise()函数容许进程向自身发送信号。
使用man 2 kill/man 3 raise
查询函数相关信息,以下:
pid取值:①pid为正数:要发送信号的进程号;②pid=0:信号被发送到全部和当前进程在同一个进程组的进程;③pid=-1:,信号发给全部的进程表中的进程(除了进程号最大的进程外);④pid<-1时,信号发送给进程组号为-pid的每个进程。
返回值:①成功:0,②失败:-1。
返回值:①成功:0,②失败:-1。
下面这个示例首先使用fork()建立了一个子进程,接着为了保证子进程不在父进程调用kill()以前退出,在子进程中使用raise()函数向自身发送SIGSTOP 信号,使子进程暂停。接下来再在父进程中调用kill()向子进程发送信号,如本示例中使用的是SIGKILL。
/* kill_raise.c */ #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t pid; int ret; if ((pid = fork()) < 0) { printf("Fork error\n"); exit(1); } if (pid == 0) { printf("Child(pid : %d) is waiting for any signal\n", getpid()); raise(SIGSTOP); exit(0); } sleep(3); if ((waitpid(pid, NULL, WNOHANG)) == 0) { if ((ret = kill(pid, SIGKILL)) == 0) { printf("Parent kill %d\n",pid); } } waitpid(pid, NULL, 0); exit(0); }
运行结果以下图所示:
(2)alarm()和pause()
alarm()也称为闹钟函数,它能够在进程中设置一个定时器,当定时器指定的时间到时,它就向进程发送SIGALARM信号。要注意的是,一个进程只能有一个闹钟时间,若是在调用alarm()以前已设置过闹钟时间,则任何之前的闹钟时间都被新值所代替。
pause()函数是用于将调用进程挂起直至捕捉到信号为止。这个函数很经常使用,一般能够用于判断信号是否已到。
该实例实际上已完成了一个简单的sleep()函数的功能,因为SIGALARM 默认的系统动做为终止该进程,所以程序在打信息以前,就会被结束了。
/* alarm_pause.c */ #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { int ret = alarm(5); pause(); printf("I have been waken up.\n",ret);//此语句不会被执行 }
『3.信号的处理』
一个进程能够决定在该进程中须要对哪些信号进行什么样的处理。例如,一个进程能够选择忽略某些信号而只处理其余一些信号,另外,一个进程还能够选择如何处理信号。总之,这些都是与特定的进程相联系的。所以,首先就要创建进程与其信号之间的对应关系,这就是信号的处理。
信号处理的主要方法有两种,一种是使用简单的signal()函数,另外一种是使用信号集函数组。下面分别介绍这两种处理方式。
(1)信号处理函数
使用signal()函数处理时,只须要指出要处理的信号和处理函数便可。它主要是用于前32种非实时信号的处理,不支持信号传递信息,可是因为使用简单、易于理解,所以也受到不少程序员的欢迎。
Linux还支持一个更健壮、更新的信号处理函数sigaction(),推荐使用该函数。
这里对函数原型进行简要说明:可先用以下的typedef进行替换说明:
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
第一个参数signum:指明了所要处理的信号类型,它能够取除了SIGKILL和SIGSTOP外的任何一种信号。
第二个参数handler:描述了与信号关联的动做,它能够取如下三种值:
①SIG_IGN:忽略该信号,②SIG_DFL:恢复对信号的系统默认处理,③sighandler_t类型的函数指针:用户自定义的处理函数。
这里着重讲解sigaction()函数中第2个和第3个参数用到的sigaction结构。可以使用man -k sigaction
查看定义:
sa_handler 是一个函数指针,指定信号处理函数,取值与signal()函数相同;
sa_mask 是一个信号集,它能够指定在信号处理程序执行过程当中哪些信号应当被屏蔽,在调用信号捕获函数以前,该信号集要加入到信号的信号屏蔽字中;
sa_flags 中包含了许多标志位,是对信号进行处理的各个选择项。
第一个实例代表了如何使用signal()函数捕捉相应信号,并作出给定的处理。这里,my_func就是信号处理的函数指针。第二个实例是用sigaction()函数
实现一样的功能。如下是使用signal()函数的示例:
/* signal.c */ #include <signal.h> #include <stdio.h> #include <stdlib.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); }
运行结果以下所示:
若使用sigaction()函数实现一样功能,main()函数作以下修改:
//sigaction.c //前部分省略 int main() { struct sigaction action; printf("Waiting for signal SIGINT or SIGQUIT...\n"); /* sigaction结构初始化 */ action.sa_handler = my_func; sigemptyset(&action.sa_mask); action.sa_flags = 0; /* 发出相应的信号,并跳转到信号处理函数处 */ sigaction(SIGINT, &action, 0); sigaction(SIGQUIT, &action, 0); pause(); exit(0); }
(2)信号集函数组
咱们已经知道,咱们能够经过信号来终止进程,也能够经过信号来在进程间进行通讯,程序也能够经过指定信号的关联处理函数来改变信号的默认处理方式,也能够屏蔽某些信号,使其不能传递给进程。那么咱们应该如何设定咱们须要处理的信号,咱们不须要处理哪些信号等问题呢?信号集函数就是帮助咱们解决这些问题的。
如下为信号集函数:
int sigemptyset(sigset_t *set); //该函数的做用是将信号集初始化为空。 int sigfillset(sigset_t *set); //该函数的做用是把信号集初始化包含全部已定义的信号。 int sigaddset(sigset_t *set, int signo); //该函数的做用是把信号signo添加到信号集set中,成功时返回0,失败时返回-1。 int sigdelset(sigset_t *set, int signo); //该函数的做用是把信号signo从信号集set中删除,成功时返回0,失败时返回-1. int sigismember(sigset_t *set, int signo); //该函数的做用是判断给定的信号signo是不是信号集中的一个成员,若是是返回1,若是不是,返回0,若是给定的信号无效,返回-1; int sigpromask(int how, const sigset_t *set, sigset_t *oset); //该函数能够根据参数指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由参数set(非空)指定,而原先的信号屏蔽字将保存在oset(非空)中。若是set为空,则how没有意义,但此时调用该函数,若是oset不为空,则把当前信号屏蔽字保存到oset中。 int sigpending(sigset_t *set); //该函数的做用是将被阻塞的信号中停留在待处理状态的一组信号写到参数set指向的信号集中,成功调用返回0,不然返回-1,并设置errno代表错误缘由。 int sigsuspend(const sigset_t *sigmask); //该函数经过将进程的屏蔽字替换为由参数sigmask给出的信号集,而后挂起进程的执行。注意操做的前后顺序,是先替换再挂起程序的执行。程序将在信号处理函数执行完毕后继续执行。若是接收到信号终止了程序,sigsuspend()就不会返回,若是接收到的信号没有终止程序,sigsuspend()就返回-1,并将errno设置为EINTR。
注意:若是一个信号被进程阻塞,它就不会传递给进程,但会停留在待处理状态,当进程解除对待处理信号的阻塞时,待处理信号就会马上被处理。
如下面的程序为例,介绍上述函数的用法:
#include <stdio.h> #include <signal.h> #include <unistd.h> void handler(int sig) { printf("Handle the signal %d\n", sig); } int main(int argc, char **argv) { sigset_t sigset; // 用于记录屏蔽字 sigset_t ign; // 用于记录被阻塞(屏蔽)的信号集 struct sigaction act; // 清空信号集 sigemptyset(&sigset); sigemptyset(&ign); // 向信号集中添加 SIGINT sigaddset(&sigset, SIGINT); // 设置处理函数 和 信号集 act.sa_handler = handler; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGINT, &act, 0); printf("Wait the signal SIGNAL...\n"); pause(); // 设置进程屏蔽字, 在本例中为屏蔽 SIGINT sigprocmask(SIG_SETMASK, &sigset, 0); printf("Please press Ctrl + C in 10 seconds...\n"); sleep(10); // 测试 SIGINT 是否被屏蔽 sigpending(&ign); if (sigismember(&ign, SIGINT)) { printf("The SIGINT signal has ignored\n"); } // 从信号集中删除信号 SIGINT sigdelset(&sigset, SIGINT); printf("Wait the signal SIGINT...\n"); // 将进程的屏蔽字从新设置, 即取消对 SIGINT 的屏蔽, 并挂起进程 sigsuspend(&sigset); printf("The process will exit in 5 seconds!\n"); sleep(5); return 0; }
运行结果以下图所示:
『1.信号量概述』
在多任务操做系统环境下,多个进程会同时运行,而且一些进程之间可能存在必定的关联。多个进程可能为了完成同一个任务会相互协做,这样造成进程之间的同步关系。并且在不一样进程之间,为了争夺有限的系统资源(硬件或软件资源)会进入竞争状态,这就是进程之间的互斥关系。
进程之间的互斥与同步关系存在的根源在于临界资源。临界资源是在同一个时刻只容许有限个(一般只有一个)进程能够访问(读)或修改(写)的资源,一般包括硬件资源(处理器、内存、存储器以及其余外围设备等)和软件资源(共享代码段,共享结构和变量等)。访问临界资源的代码叫作临界区,临界区自己也会成为临界资源。
信号量是用来解决进程之间的同步与互斥问题的一种进程之间通讯机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操做(PV操做)。其中信号量对应于某一种资源,取一个非负的整型值。信号量值指的是当前可用的该资源的数量,若它等于0 则意味着目前没有可用的资源。PV原子操做的具体定义以下:
最简单的信号量是只能取0和1两种值,这种信号量被叫作二维信号量。在这里,咱们主要讨论二维信号量。
『2.信号量的应用』
(1)函数说明:
在Linux系统中,使用信号量一般分为如下几个步骤:
(2)函数格式:
(3)使用实例:
下面的程序展现了信号量的概念及其基本用法。在示例程序中,使用信号量来控制父子进程之间的执行顺序。
方便起见,咱们将信号量相关的函数封装成二维单个信号量的几个基本函数。它们分别为为信号量初始化函数(或者信号量赋值函数)init_sem()、P 操做数sem_p()、V操做函数sem_v()以及删除信号量的函数del_sem()等,具体实现以下所示:
/* sem_com.c */ #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> union semun { int val; struct semid_ds *buf; unsigned short *array; }; /*信号量初始化(赋值)函数*/ int init_sem(int sem_id, int init_value) { union semun sem_union; sem_union.val = init_value; //init_value为初始值 if (semctl(sem_id, 0, SETVAL, sem_union) == -1) { perror("Initialize semaphore"); return -1; } return 0; } /*从系统中删除信号量的函数*/ int del_sem(int sem_id) { union semun sem_union; if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1) { perror("Delete semaphore"); return -1; } } /*P操做函数*/ int sem_p(int sem_id) { struct sembuf sem_b; /*单个信号量的编号应该为0 */ sem_b.sem_num = 0; /*表示P操做*/ sem_b.sem_op = -1; /*系统自动释放将会在系统中残留的信号量*/ sem_b.sem_flg = SEM_UNDO; if (semop(sem_id, &sem_b, 1) == -1) { perror("P operation"); return -1; } return 0; } /*V操做函数*/ int sem_v(int sem_id) { struct sembuf sem_b; sem_b.sem_num = 0; sem_b.sem_op = 1; sem_b.sem_flg = SEM_UNDO; if (semop(sem_id, &sem_b, 1) == -1) { perror("V operation"); return -1; } return 0; }
下面编写一个测试程序,调用这些简单易用的接口,从而解决控制两个进程之间的执行顺序的同步问题。代码以下:
/* fork.c */ #include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #define DELAY_TIME 3 /*为了突出演示效果,等待几秒钟*/ int main(void) { pid_t result; int sem_id; sem_id = semget(ftok(".", 'a'), 1, 0666|IPC_CREAT); /*建立一个信号量*/ init_sem(sem_id, 0); /*调用fork()函数*/ result = fork(); if(result == -1) { perror("Fork\n"); } else if (result == 0) //返回值为0表明子进程 { printf("Child process will wait for some seconds...\n"); sleep(DELAY_TIME); printf("The returned value is %d in the child process(PID = %d)\n", result, getpid()); sem_v(sem_id); } else //返回值大于0表明父进程 { sem_p(sem_id); printf("The returned value is %d in the father process(PID = %d)\n", result, getpid()); sem_v(sem_id); del_sem(sem_id); } exit(0); }
能够先从fork.c中删去信号量相关的代码部分查看运行结果:
再添加信号量的控制部分并运行结果:
『1.共享内存概述』
采用共享内存通讯的一个显而易见的好处是效率高,由于进程能够直接读写内存,而不须要任何数据的拷贝。对于像管道和消息队列等通讯方式,则须要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另外一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不老是读写少许数据后就解除映射,有新的通讯时,再从新创建共享内存区域。而是保持共享区域,直到通讯完毕为止,这样,数据内容一直保存在共享内存中,并无写回文件。共享内存中的内容每每是在解除映射时才写回文件的。所以,采用共享内存的通讯方式效率是很是高的。
Linux的2.2.x内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及系统V共享内存。linux发行版本如Redhat 8.0支持mmap()系统调用及系统V共享内存,但还没实现Posix共享内存,接下来将主要介绍mmap()系统调用及系统V共享内存API的原理及应用。
『2.mmap()系统调用』
(1)函数说明:
mmap()系统调用使得进程之间经过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程能够向访问普通内存同样对文件进行访问,没必要再调用read(),write()等操做。
注:实际上,mmap()系统调用并非彻底为了用于共享内存而设计的。它自己提供了不一样于通常对普通文件的访问方式,进程能够像读写内存同样对普通文件的操做。而Posix或系统V的共享内存IPC则纯粹用于共享目的,固然mmap()实现共享内存也是其主要应用之一。
(2)函数格式:
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
参数含义:
返回值:若映射成功则返回映射区的内存起始地址,不然返回MAP_FAILED(-1),错误缘由存于errno 中。
munmap():
int munmap( void * addr, size_t len )
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将致使段错误发生。
int msync ( void * addr , size_t len, int flags)
通常说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,每每在调用munmap()后才执行该操做。能够经过调用msync()实现磁盘上文件内容与共享内存区的内容一致。
(3)使用实例:
下面给出两个进程经过映射普通文件实现共享内存通讯。
示例包含两个子程序:map_normalfile1.c及map_normalfile2.c。编译两个程序,可执行文件分别为map_normalfile1及map_normalfile2。两个程序经过命令行参数指定同一个文件来实现共享内存方式的进程间通讯。
map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操做。
map_normalfile1把命令行参数指定的文件映射到进程地址空间,而后对映射后的地址空间执行读操做。这样,两个进程经过命令行参数指定同一个文件来实现共享内存方式的进程间通讯。
/*-------------map_normalfile1.c-----------*/ #include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> typedef struct{ char name[4]; int age; }people; main(int argc, char** argv) // map a normal file as shared mem: { int fd,i; people *p_map; char temp; fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777); lseek(fd,sizeof(people)*5-1,SEEK_SET); write(fd,"",1); p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE, MAP_SHARED,fd,0 ); close( fd ); temp = 'a'; for(i=0; i<10; i++) { temp += 1; memcpy( ( *(p_map+i) ).name, &temp,2 ); ( *(p_map+i) ).age = 20+i; } printf(" initialize over \n "); sleep(10); munmap( p_map, sizeof(people)*10 ); printf( "umap ok \n" ); } /*-------------map_normalfile2.c-----------*/ #include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> typedef struct{ char name[4]; int age; }people; main(int argc, char** argv) // map a normal file as shared mem: { int fd,i; people *p_map; fd=open( argv[1],O_CREAT|O_RDWR,00777 ); p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE, MAP_SHARED,fd,0); for(i = 0;i<10;i++) { printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age ); } munmap( p_map,sizeof(people)*10 ); }
map_normalfile1.c首先定义了一个people数据结构,(在这里采用数据结构的方式是由于,共享内存区的数据每每是有固定格式的,这由通讯的各个进程决定,采用结构的方式有广泛表明性)。map_normfile1首先打开或建立一个文件,并把文件的长度设置为5个people结构大小。而后从mmap()的返回地址开始,设置了10个people结构。而后,进程睡眠10秒钟,等待其余进程映射同一个文件,最后解除映射。
map_normfile2.c只是简单的映射一个文件,并以people数据结构的格式从mmap()返回的地址处读取10个people结构,并输出读取的值,而后解除映射。
分别把两个程序编译成可执行文件map_normalfile1和map_normalfile2后,在一个终端上先运行./map_normalfile1 ./test,程序输出结果以下:
在map_normalfile1输出initialize over以后,输出umap ok以前,在另外一个终端上运行map_normalfile2 /tmp/test_shm,将会产生以下输出(为了节省空间,输出结果为稍做整理后的结果):
name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24; name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;
在map_normalfile1 输出umap ok后,运行map_normalfile2则输出以下结果:
name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24; name: age 0; name: age 0; name: age 0; name: age 0; name: age 0;
『3.System V共享内存』
(1)函数说明:
共享内存的实现分为两个步骤,第一步是建立共享内存,这里用到的函数是shmget(),也就是从内存中得到一段共享内存区域,第二步映射共享内存,也就是把这段建立的共享内存映射到具体的进程空间中,这里使用的函数是shmat()。到这里,就可使用这段共享内存了,也就是可使用不带缓冲的I/O 读写命令对其进行操做。除此以外,固然还有撤销映射的操做,其函数为shmdt()。这里就主要介绍这3个函数。
(2)函数格式:
(3)使用实例:
下面的程序将给出System V共享内存API的使用方法,并对比分析System V共享机制与mmap()映射普通文件实现共享内存之间的差别。首先给出两个进程经过System V共享内存通讯的范例:
/***** testwrite.c *******/ #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <unistd.h> typedef struct{ char name[4]; int age; } people; main(int argc, char** argv) { int shm_id,i; key_t key; char temp; people *p_map; char* name = "./test"; key = ftok(name,0); if(key==-1) perror("ftok error"); shm_id=shmget(key,4096,IPC_CREAT); if(shm_id==-1) { perror("shmget error"); return; } p_map=(people*)shmat(shm_id,NULL,0); temp='a'; for(i = 0;i<10;i++) { temp+=1; memcpy((*(p_map+i)).name,&temp,1); (*(p_map+i)).age=20+i; } if(shmdt(p_map)==-1) perror(" detach error "); }
/********** testread.c ************/ #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <unistd.h> typedef struct{ char name[4]; int age; } people; main(int argc, char** argv) { int shm_id,i; key_t key; people *p_map; char* name = "./test"; key = ftok(name,0); if(key == -1) perror("ftok error"); shm_id = shmget(key,4096,IPC_CREAT); if(shm_id == -1) { perror("shmget error"); return; } p_map = (people*)shmat(shm_id,NULL,0); for(i = 0;i<10;i++) { printf( "name:%s\n",(*(p_map+i)).name ); printf( "age %d\n",(*(p_map+i)).age ); } if(shmdt(p_map) == -1) perror(" detach error "); }
testwrite.c建立一个系统V共享内存区,并在其中写入格式化数据;testread.c访问同一个系统V共享内存区,读出其中的格式化数据。分别把两个程序编译为testwrite及testread,前后执行./testwrite及./testread则./testread运行结果以下:
经过对试验结果分析,对比系统V与mmap()映射普通文件实现共享内存通讯,能够得出以下结论:
『1.消息队列概述』
消息队列,即一些消息的列表。用户能够从消息队列中添加消息和读取消息等。从这点上看消息队列具备必定的FIFO特性,可是它能够实现消息的随机查询,比FIFO具备更大的优点。同时,消息又是存在于内核中的,由“队列ID”来标识。
『2.消息队列的应用』
(1)函数说明:
消息队列的实现包括建立或打开消息队列、添加消息、读取消息和制消息队列这4种操做。
(2)函数格式:
(3)使用实例:
这里首先介绍一个函数ftok(),它能够根据不一样的路径和关键字产生标准的key。共享内存,消息队列,信号量它们三个都是找一个中间介质,来进行通讯的,而使用ftok()产生一个号,就能够惟一区分这个介质了。
ftok()函数的具体形式以下:
key_t ftok(const char *pathname, int proj_id);
其中参数fname是指定的文件名,这个文件必须是存在的并且能够访问的。id是子序号,它是一个8bit的整数。即范围是0~255。当函数执行成功,则会返回key_t键值,不然返回-1。在通常的UNIX中,一般是将文件的索引节点取出,而后在前面加上子序号就获得key_t的值。
对于该函数,还有如下几点补充说明:
如下是一个简单的验证程序:
#include <stdio.h> #include <sys/sem.h> #include <stdlib.h> int main() { key_t semkey; if((semkey = ftok("./test", 1))<0) { printf("ftok failed\n"); exit(EXIT_FAILURE); } printf("ftok ok ,semkey = %d\n", semkey); return 0; }
运行结果以下:
言归正传。下面实例体现了如何使用消息队列进行两个进程(发送端和接收端)之间的通讯,包括消息队列的建立、消息发送与读取、消息队列的撤销和删除等多种操做。
/* msgsnd.c 消息队列发送端*/ #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define BUFFER_SIZE 512 struct message { long msg_type; char msg_text[BUFFER_SIZE]; }; int main() { int qid; key_t key; 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("Open queue %d\n",qid); while(1) { printf("Enter some message to the queue:"); if ((fgets(msg.msg_text, BUFFER_SIZE, stdin)) == NULL) { puts("no message"); exit(1); } msg.msg_type = getpid(); /*添加消息到消息队列*/ if ((msgsnd(qid, &msg, strlen(msg.msg_text), 0)) < 0) { perror("message posted"); exit(1); } if (strncmp(msg.msg_text, "quit", 4) == 0) { break; } } exit(0); }
/* msgrcv.c 消息队列接收端*/ #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define BUFFER_SIZE 512 struct message { long msg_type; char msg_text[BUFFER_SIZE]; }; int main() { int qid; key_t key; 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("Open queue %d\n", qid); do { /*读取消息队列*/ memset(msg.msg_text, 0, BUFFER_SIZE); if (msgrcv(qid, (void*)&msg, BUFFER_SIZE, 0, 0) < 0) { perror("msgrcv"); exit(1); } printf("The message from process %d : %s", msg.msg_type, msg.msg_text); } while(strncmp(msg.msg_text, "quit", 4)); /*从系统内核中移走消息队列*/ if ((msgctl(qid, IPC_RMID, NULL)) < 0) { perror("msgctl"); exit(1); } exit(0); }
如下是程序的运行结果,输入“quit”则两个进程都结束。