Linux环境下,进程地址空间相互独立,每一个进程各自有不一样的用户地址空间。任何一个进程的全局变量在另外一个进程中都看不到,因此进程和进程之间不能相互访问,要交换数据必须经过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通讯(IPC,InterProcess Communication)。shell
在进程间完成数据传递须要借助操做系统提供特殊的方法,如:管道、命名管道、信号、消息队列、共享内存、信号量、套接字等。随着计算机的蓬勃发展,一些方法因为自身设计缺陷被淘汰或者弃用。现今经常使用的进程间通讯方式有:服务器
管道的通讯方式为:写端每次都将数据写入管道缓冲区的 末尾 ,而读端每次都从管道缓冲区的 头部 读出数据。数据结构
管道的实质是内核利用 环形队列 的数据结构在 内核缓冲区 中的一个实现,默认设置大小为4K,能够经过ulimit -a
命令查看。因为利用 环形队列 进行实现,读和写的位置都是自动增加的,不能随意改变,一个数据只能被读取一次,读取后数据就会从缓冲区中移除。当缓冲区读空或者写满时,有必定的规则控制相应的读进程或者写进程进入等待队列,当空的缓冲区有新数据写入或者满的缓冲区有数据读出来时,就唤醒等待队列中的进程继续读写。异步
没有名称函数
建立管道操作系统
int pipe(int pipefd[2]); //成功:0;失败:-1,设置errno
设计
函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出同样。向管道文件读写数据实际上是在读写内核缓冲区指针
管道建立成功之后,建立该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通讯呢?一般能够采用以下步骤:code
eg.server
#include <unistd.h> #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char *argv[]) { int fd[2]; int res = pipe(fd); if(res == -1) { perror("pipe error"); return -1; } pid_t pid = fork(); if(pid == 0){ // child close(fd[1]);// close write char buf[100]; int count = 0; while(count < 5) { memset(buf,0,sizeof(buf)); int res = read(fd[0], buf, sizeof(buf)); if(res > 0) { buf[res - 1] = '\0'; } printf("I am child! Recv msg:%s from father\n", buf); ++count; } close(fd[0]); }else if(pid > 0){ close(fd[0]);//close read int count = 0; char buf[100]; while(count < 5) { memset(buf,0,sizeof(buf)); sprintf(buf,"count = %d", ++count); int res = write(fd[1],buf,strlen(buf) + 1);// 多一个字节存放\0 printf("I am father! Send msg:%s to child!\n",buf); sleep(2); } close(fd[1]); if(waitpid(pid, NULL, 0) < 0) { return -1; } }else{ perror("fork error!"); return -1; } return 0; }
运行结果:
使用管道须要注意如下4种特殊状况(假设都是阻塞I/O操做,没有设置O_NONBLOCK标志):
总结:
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但经过FIFO,不相关的进程也能交换数据。
命名管道不一样于匿名管道之处在于它提供了一个路径名与之关联,以命名管道的文件形式存在于文件系统中,这样,即便与命名管道的建立进程不存在亲缘关系的进程,只要能够访问该路径,就可以彼此经过命名管道相互通讯,所以,经过命名管道不相关的进程也能交换数据。值的注意的是,命名管道严格遵循先进先出(first in first out),对匿名管道及有名管道的读老是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操做。命名管道的名字存在于文件系统中,内容存放在内存中。
命名管道阻塞问题:命名管道在打开时须要确实对方的存在,不然将阻塞。即以读方式打开某管道,在此以前必须一个进程以写方式打开管道,不然阻塞。此外,能够以读写(O_RDWR)模式打开命名管道,即当前进程读,当前进程写,不会阻塞。
库函数:
int mkfifo(const char *pathname, mode_t mode); //成功:0; 失败:-1
一旦使用mkfifo建立了一个FIFO,就可使用open打开它,常见的文件I/O函数均可用于fifo。如:close、read、write、unlink等。
eg.
fifo_w.c // 向FIFO中写入数据
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <string.h> #include <fcntl.h> #define MAX_BUF_SIZE 655360 int main(int argc, char* argv[]) { if(argc < 2) { fprintf(stderr,"usage: %s argv[1].\n",argv[0]); return -1; } if(mkfifo(argv[1], 0666) < 0 && errno != EEXIST) { fprintf(stderr,"Fail to mkfifo %s : %s.",argv[1],strerror(errno)); return -1; } int fd; if((fd = open(argv[1],O_WRONLY)) < 0) { fprintf(stderr,"Fail to open mkfifo %s : %s.",argv[1],strerror(errno)); return -1; } printf("open for write success\n"); int n; char buf[MAX_BUF_SIZE]; while(1) { memset(buf, 0, sizeof(buf)); printf(">"); scanf("%s",buf); n = write(fd, buf, strlen(buf) + 1);// 将\0也写入 printf("Write %d bytes.\nSend MSG:%s\n",n, buf); } return 0; }
fifo_r.c // 从FIFO中读取数据
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <errno.h> #define MAX_BUF_SIZE 655360 int main(int argc, char *argv[]) { if(argc < 2) { fprintf(stderr,"Usage: %s argv[1]\n",argv[0]); return -1; } if(mkfifo(argv[1],0666) < 0 && errno != EEXIST) { fprintf(stderr,"Fail to mkfifo %s : %s",argv[1],strerror(errno)); return -1; } int fd; if((fd = open(argv[1],O_RDONLY)) < 0) { fprintf(stderr,"Fail to open mkfifo %s : %s",argv[1],strerror(errno)); return -1; } int n; char buf[MAX_BUF_SIZE]; while(1) { memset(buf, 0, sizeof(buf)); n = read(fd, buf, sizeof(buf)); printf("Read %d bytes\nRECV MSG:%s\n",n, buf); } return 0; }
运行结果:
写入:
读取:
Linux系统中经常使用信号:
- SIGHUP:用户从终端注销,全部已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。
- SIGINT:程序终止信号。程序运行过程当中,按
Ctrl+C
键将产生该信号。- SIGQUIT:程序退出信号。程序运行过程当中,按
Ctrl+\
键将产生该信号。- SIGBUS和SIGSEGV:进程访问非法地址。
- SIGFPE:运算中出现致命错误,如除零操做、数据溢出等。
- SIGKILL:用户终止进程执行信号。shell下执行
kill -9
发送该信号。- SIGTERM:结束进程信号。shell下执行
kill 进程pid
发送该信号。- SIGALRM:定时器信号。
- SIGCLD:子进程退出信号。若是其父进程没有忽略该信号也没有处理该信号,则子进程退出后将造成僵尸进程。
可使用kill -l
查看当前系统可用信号有哪些
信号是软件层次上对中断机制的一种模拟,是一种异步通讯方式,,信号能够在用户空间进程和内核之间直接交互,内核能够利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:
Ctrl+C
退出、硬件异常如无效的存储访问等。kill
函数、软件异常产生信号。eg.
借助SIGCHLD信号回收子进程
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <signal.h> void sys_err(char *str) { perror(str); exit(1); } void do_sig_child(int signo) { int status; pid_t pid; while ((pid = waitpid(0, &status, WNOHANG)) > 0) { if (WIFEXITED(status)) printf("child %d exit %d\n", pid, WEXITSTATUS(status)); else if (WIFSIGNALED(status)) printf("child %d cancel signal %d\n", pid, WTERMSIG(status)); } } int main(void) { pid_t pid; int i; for (i = 0; i < 10; i++) { if ((pid = fork()) == 0) break; else if (pid < 0) sys_err("fork"); } if (pid == 0) { int n = 1; while (n--) { printf("child ID %d\n", getpid()); sleep(1); } return i+1; } else if (pid > 0) { struct sigaction act; act.sa_handler = do_sig_child; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGCHLD, &act, NULL); while (1) { printf("Parent ID %d\n", getpid()); sleep(1); } } return 0; }
运行结果:
另外与管道不一样的是,消息队列在某个进程往一个队列写入消息以前,并不须要另外某个进程在该队列上等待消息的到达
消息队列特色总结:
- 消息队列是消息的链表,具备特定的格式,存放在内存中并由消息队列标识符标识.
- 消息队列容许一个或多个进程向它写入与读取消息
- 管道和消息队列的通讯数据都是先进先出的原则。
- 消息队列能够实现消息的随机查询,消息不必定要以先进先出的次序读取,也能够按消息的类型读取.比FIFO更有优点。
- 消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
- 目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,System V消息队列目前被大量使用。System V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。
对于系统中的每一个消息队列,内核维护一个定义在<sys/msg.h>
头文件中的信息结构。
struct msqid_ds { struct ipc_perm msg_perm ; struct msg* msg_first ; //指向队列中的第一个消息 struct msg* msg_last ; //指向队列中的最后一个消息 …… } ;
调用的第一个函数一般是msgget
,其功能是打开一个现存队列或建立一个新队列。
#include <sys/msg.h> int msgget (key_t key, int oflag) ;
返回值是一个整数标识符,其余三个msg函数就用它来指代该队列。它是基于指定的key产生的,而key既能够是ftok的返回值,也能够是常值IPC_PRIVATE。
oflag是读写权限的组合(用于打开时)。它还能够是IPC_CREATE或IPC_CREATE | IPC_EXCL(用于建立时)。
使用msgsnd
打开一个消息队列后,咱们使用msgsnd
往其上放置一个消息。
#include <sys/msg.h> int msgsnd (int msqid, const void *ptr, size_t length, int flag) ;
其中msqid是由msgget返回的标识符。ptr是一个结构指针,该结构具备以下模板(咱们须要按这个模板本身定义结构体)
struct mymesg { long mtype ; //消息类型(大于0) char mtext[512] ; //消息数据 } ; //结构体的名字和其中变量名都由咱们本身肯定,咱们只要按照这个模板定义便可。
消息数据mtext中,任何形式的数据都是容许的,不管是二进制数据仍是文本,内核根本不解释消息数据的内容。(咱们能够在消息的数据部分 再分割一部分 根据须要定义本身的通讯协议)
参数length指定了待发送消息数据部分的长度。
参数flag的值能够指定为IPC_NOWAIT。这相似于文件IO的非阻塞IO标志。若消息队列已满,则指定IPC_NOWAIT使得msgsnd当即出错返回EAGAIN。
若是没有指定IPC_NOWAIT,则进程阻塞直到下述状况出现为止:①有空间能够容纳要发送的消息 ②从系统中删除了此队列(返回EIDRM“标识符被删除”)③捕捉到一个信号,并从信号处理程序返回(返回EINTR)
使用msgrcv函数从某个消息队列中读出一个消息。
#include <sys/msg.h> ssize_t msgrcv (int msqid, void* ptr, size_t length, long type, int flag) ;
参数ptr指定所接收消息的存放位置。参数length指定了数据部分大小(只想要多长的数据)
参数type指定但愿从队列中读出什么样的消息。
type == 0 返回队列中的第一个消息
type > 0 返回队列中消息类型为type的第一个消息
type < 0 返回队列中消息类型值小于或等于type绝对值的消息,若是这种消息有若干个。则取类型值最小的消息。
(若是一个消息队列由多个客户进程和一个服务器进程使用,那么type字段能够用来包含客户进程的进程ID)
参数flag能够指定为IPC_NOWAIT,使操做不阻塞。
msgctl函数提供在一个消息队列上的各类控制操做。
#include <sys/msg.h> int msgctl (int msqid, in cmd, struct msqid_ds * buff) ;
参数cmd说明对由msqid指定的队列要执行的命令:
IPC_STAT :取此队列的msqid_ds结构,并将它存放在buf指向的结构中。
IPC_SET :按由buf指向结构中的值,设置与此队列相关结构中的字段。
IPC_RMID:从系统中删除该消息队列以及仍在该队列中的全部数据。
(这三条命令也可用于信号量和共享存储)
eg.
一个写进程,多个读进程
//-------------------头文件msgqueue.h ------------------ #ifndef _MAGQUEUE_H_ #define _MAGQUEUE_H_ #include <sys/ipc.h> //包含ftok #include <sys/msg.h> #include <sys/types.h> //消息队列的读 写模式掩码 #define MSG_W 0200 #define MSG_R 0400 //定义众所周知的消息队列键 #define MQ_KEY1 128L #define DATA_SIZE 512 typedef struct msgbuf { long mtype ; char mdata[DATA_SIZE] ; } mymsg_t ; #endif //-----------------客户端进程----------------- #include "msgqueue.h" #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> void client(int, int) ; int main(int argc, char** argv) { int msgqid ; //打开消息队列 msgqid = msgget(MQ_KEY1, 0) ; if (msgqid < 0) { puts("Open msg queue error!\n") ; exit(0) ; } client(msgqid, msgqid) ; exit(0) ; } void client(int readfd, int writefd) { mymsg_t msgToServer ; mymsg_t msgFromServer ; char* writePtr ; ssize_t pidLen ; ssize_t dataLen ; ssize_t recvBytes ; int pid ; //-------构造一条消息----- //消息类型为1 msgToServer.mtype = 1 ; //在消息头部放本进程ID和空格 pid = getpid() ; snprintf(msgToServer.mdata, DATA_SIZE, "%ld ", pid) ; pidLen = strlen(msgToServer.mdata) ; writePtr = msgToServer.mdata + pidLen ; //从标准输入读入文件路径 fgets(writePtr, DATA_SIZE - pidLen, stdin) ; dataLen = strlen(msgToServer.mdata) ; if (msgToServer.mdata[dataLen-1] == '\n') //删除换行符 { msgToServer.mdata[dataLen-1] = '\0' ; } //发送消息 if (msgsnd(writefd, &msgToServer, strlen(msgToServer.mdata), 0) == -1) { puts("Send Error!"); exit(0) ; } //-----接收来自服务器的消息 while ((recvBytes = msgrcv(readfd, &msgFromServer, DATA_SIZE, pid, 0)) > 0) { write(STDOUT_FILENO, msgFromServer.mdata, recvBytes) ; } } //---------------服务器端进程--------------- //消息队列是双向通讯的,故用单个队列就够用。 //咱们用每一个消息的类型来标识该消息是从客户到服务器,仍是从服务器到客户。 //客户向队列发类型为一、PID和路径名。 //服务器向队列发类型为客户进程ID的文件内容。 // //当心死锁隐患: //客户们能够填满消息队列,妨碍服务器发送应答,因而客户被阻塞在发送中,服务器也被阻塞。 //避免的方法是:约定服务器对消息队列老是使用非阻塞写。 #include "msgqueue.h" #include <stdio.h> #include <stdlib.h> #include <string.h> void server(int, int) ; int main(int argc, char** argv) { int msgqid; //建立消息队列 msgqid = msgget(MQ_KEY1, IPC_CREAT) ; if (msgqid < 0) { puts("Create msg queue error!\n") ; exit(0) ; } server(msgqid, msgqid) ; exit(0) ; } void server(int readfd, int writefd) { FILE* fp ; pid_t clientPid ; mymsg_t* msgPtr ; ssize_t recvBytes ; char* pathStr ; while(1) { //从消息队列中读取来自客户的请求文件路径 msgPtr = malloc(DATA_SIZE + sizeof(long)) ; recvBytes = msgrcv(readfd, msgPtr, DATA_SIZE, 1, 0) ; //阻塞读 if (recvBytes <= 0) { puts("pathname missing") ; continue ; } msgPtr->mdata[recvBytes] = '\0' ; //分析消息,提取客户PID,文件路径 if ((pathStr = strchr(msgPtr->mdata, ' ')) == NULL) { puts("bogus request!") ; continue ; } *pathStr++ = 0 ; clientPid = atol(msgPtr->mdata) ; //读取文件内容 返回给客户 msgPtr->mtype = clientPid ; //msgPtr既做为接收消息 又用做发送消息 if ((fp = fopen(pathStr, "r")) == NULL) { //读取文件失败,返回给客户失败信息(在原消息内容后 添加错误信息) snprintf(msgPtr->mdata + recvBytes, sizeof(msgPtr->mdata) -recvBytes, ": can't open!") ; if (msgsnd(writefd, msgPtr, strlen(msgPtr->mdata), IPC_NOWAIT) == -1) { puts("Send Error!"); exit(0); } } else { //copy文件内容 发给客户 while (fgets(msgPtr->mdata, DATA_SIZE, fp) != NULL) { msgsnd(writefd, msgPtr, strlen(msgPtr->mdata), IPC_NOWAIT) ; //非阻塞写 } } }//while() }