一.进程间通信
-
进程间通信的目的
- **数据传输:**一个进程需要将它的数据发送给另一个进程(cat log.txt | grep hello)。
- **资源共享:**多个进程之间共享同样的资源。
- **通知事件:**一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- **进程控制:**有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
-
进程间通信的本质
-
进程间通信的本质是让 不同的进程看到同一份资源(内存 , 文件,内核缓冲等)
资源由谁(OS的哪些模块)提供 , 就有了不同的进程间通信方式!
这里的模块可以是: (文件–管道) , (OS内核IPC提供- SystemV IPC) , (网络–套接字)
-
a.进程运行的时候是具有独立性的!(数据层面) , 因此进程之间要实现通信是非常困难的。
b.进程间通信,一般一定要借助第三方(OS)资源。
c.通信的本质就是”数据的拷贝“。 -
参考:CSDN:作者:GSX_M:进程间通信详解(http://t.csdnimg.cn/GWh4X)
-
-
进程间通信分类
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。
二.无名管道
-
无名管道
管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。
-
特点:
- 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。管道数据读被走就没了。
- 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
- 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
-
函数原型:
#include <unistd.h> int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1
-
当一个管道建立时,它会创建两个文件描述符:
fd[0]
为读而打开,fd[1]
为写而打开。如下图:要关闭管道只需将这两个文件描述符关闭即可。
-
示例:
单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。如下图所示:
若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { int fd[2];//fd[0]read,fd[1]write char buf[20] = {"hello world!"}; char buf2[20] = {0}; pid_t pid; if((pipe(fd)) == -1){ printf("pipe create failed!\n"); } pid = fork(); if(pid < 0){ printf("fork create failed!\n"); } if(pid > 0){ printf("this is father process\n"); close(fd[0]); //ssize_t write(int fd, const void *buf, size_t count); write(fd[1],buf,20); wait(); }if(pid == 0){ printf("this is child process\n"); close(fd[1]); //ssize_t read(int fd, void *buf, size_t count); read(fd[0],buf2,20); printf("read:%s\n",buf2); exit(0); } return 0; }
FIFO,也称为命名管道,它是一种文件类型。
三.命名管道
FIFO,也称为命名管道,它是一种文件类型。
-
特点
- FIFO可以在无关的进程之间交换数据,与无名管道不同。
- FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
-
原型
#include <sys/stat.h> // 返回值:成功返回0,出错返回-1 int mkfifo(const char *pathname, mode_t mode);
其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。
当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:
- 若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
- 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。
-
示例:
-
创建fifo
#include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <errno.h> //int mkfifo(const char *pathname, mode_t mode); int main() { if((mkfifo("./file1",0600))==-1 && errno !=EEXIST){ printf("mkfifo failed!\n"); perror("why:"); } return 0; }
-
write函数
#include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <errno.h> #include <fcntl.h> #include <string.h> #include <stdlib.h> int main() { int fd; int i = 5; char *str = "message from write"; if((fd = open("./file1",O_WRONLY)) < 0){ printf("open fifo failed!\n"); exit(-1); } while(i){ write(fd,str,strlen(str)); sleep(1); i--; } close(fd); return 0; }
-
read函数
#include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <errno.h> #include <fcntl.h> #include <stdlib.h> int main() { int fd; char buf[30]; int nread; if((fd = open("./file1",O_RDONLY)) < 0){ printf("open fifo failed!\n"); exit(-1); } while((nread = read(fd,buf,30)) > 0){ printf("read %d bytes from file1\n",nread); printf("content:%s\n",buf); } close(fd); return 0; }
-
四.消息队列
消息队列(message queue),是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
-
特点
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
-
函数原型
#include <sys/msg.h> // 创建或打开消息队列:成功返回队列ID,失败返回-1 int msgget(key_t key, int flag); // 添加消息:成功返回0,失败返回-1 int msgsnd(int msqid, const void *ptr, size_t size, int flag); // 读取消息:成功返回消息数据的长度,失败返回-1 int msgrcv(int msqid, void *ptr, size_t size, long type,int flag); // 控制消息队列:成功返回0,失败返回-1 int msgctl(int msqid, int cmd, struct msqid_ds *buf);
在以下两种情况下,msgget将创建一个新的消息队列:
- 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
- key参数为IPC_PRIVATE。
函数msgrcv在读取消息队列时,type参数有下面几种情况:
- type == 0,返回队列中的第一个消息;
- type > 0,返回队列中消息类型为 type 的第一个消息;
- type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。
可以看出,type值非 0 时用于以非先进先出次序读消息。也可以把 type 看做优先级的权值。
-
示例:
msgrcv
#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdlib.h> #include <string.h> struct msgbuf {//消息队列结构体 long mtype; /* message type, must be > 0 */ char mtext[128]; /* message data */ }; int main() { int msgid; struct msgbuf msg_readbuf; struct msgbuf msg_sendbuf = {999,"888receive,return999!"}; key_t key; key = ftok(".",'z');//ftok是获得key值函数 printf("the key is %x\n",key); if((msgid = msgget(key,IPC_CREAT|0777)) == -1){ printf("create msg_que failed!\n"); exit(-1); } msgrcv(msgid,&msg_readbuf,sizeof(msg_readbuf.mtext),888,0); printf("888get msg:%s\n",msg_readbuf.mtext); msgsnd(msgid,&msg_sendbuf,strlen(msg_sendbuf.mtext),0); printf("999send success!\n"); msgctl(msgid,IPC_RMID,NULL);//清除消息队列 return 0; }
msgsnd
#include <stdio.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdlib.h> #include <string.h> struct msgbuf { long mtype; /* message type, must be > 0 */ char mtext[128]; /* message data */ }; int main() { int msgid; struct msgbuf msg_sendbuf = {888,"this is a message from 888!"}; struct msgbuf msg_readbuf; key_t key; key = ftok(".",'z'); printf("the key is %x\n",key); if((msgid = msgget(0x1235,IPC_CREAT|0777)) == -1){ printf("create msg_que failed!\n"); exit(-1); } msgsnd(msgid,&msg_sendbuf,strlen(msg_sendbuf.mtext),0); printf("888send success!\n"); msgrcv(msgid,&msg_readbuf,sizeof(msg_readbuf.mtext),999,0); printf("999get msg:%s\n",msg_readbuf.mtext); msgctl(msgid,IPC_RMID,NULL); return 0; }
五.共享内存
共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。
-
特点
- 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
- 因为多个进程可以同时操作,所以需要进行同步。
- 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
-
原型
#include <sys/shm.h> // 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1 int shmget(key_t key, size_t size, int flag); // 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1 void *shmat(int shm_id, const void *addr, int flag); // 断开与共享内存的连接:成功返回0,失败返回-1 int shmdt(void *addr); // 控制共享内存的相关信息:成功返回0,失败返回-1 int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
当用shmget函数创建一段共享内存时,必须指定其 size;而如果引用一个已存在的共享内存,则将 size 指定为0 。
当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。
shmdt函数是用来断开shmat建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。
shmctl函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。
-
示例
shm_write
#include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <string.h> #include <stdlib.h> //int shmget(key_t key, size_t size, int shmflg); //void *shmat(int shmid, const void *shmaddr, int shmflg); //int shmdt(const void *shmaddr); int main() { int shmid; char *shmaddr; key_t key; key = ftok(".",1); if((shmid = shmget(key,1024*4,IPC_CREAT|0666)) == -1){//创建共享内存 printf("create shm failed!\n"); exit(-1); } shmaddr = shmat(shmid,0,0);//获取共享内存 printf("shmat OK!\n"); strcpy(shmaddr,"this is share memory!"); sleep(5); shmdt(shmaddr);//卸载共享内存 shmctl(shmid,IPC_RMID,0);//删除共享内存 printf("quit\n"); return 0; }
shm_read
#include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <string.h> #include <stdlib.h> int main() { int shmid; char *shmaddr; key_t key; key = ftok(".",1); if((shmid = shmget(key,1024*4,0)) == -1){ printf("shmid : %d\n",shmid); printf("create shm failed!\n"); exit(-1); } shmaddr = shmat(shmid,0,0); printf("shmat OK!\n"); printf("read from shm : %s\n",shmaddr);//直接读取,打印 sleep(1); shmdt(shmaddr); printf("quit!\n"); return 0; }
ipcs -m指令查看共享内存
六.Linux信号(Signal)
对于Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号,为Linux提供了一种处理异步事件的方法。比如,终端用户输入了Ctrl+c来中断程序,会通过信号机制停止一个程序。
-
信号的定义和作用
-
信号的定义:
信号是 Linux 操作系统中用于进程间通信、处理异常等情况的一种机制。它是由操作系统向一个进程或者线程发送的一种异步通知,用于通知该进程或线程某种事件已经发生,需要做出相应的处理。
-
信号的作用:
- **进程间通信:**进程可以通过向其他进程发送信号的方式进行通信,例如某个进程在完成了某项工作之后,可以向另一个进程发送 SIGUSR1 信号,通知其进行下一步的操作。
- **处理异常:**信号可以被用来处理程序中的异常情况,例如当一个进程尝试访问未分配的内存或者除以 0 时,系统会向该进程发送 SIGSEGV 或 SIGFPE 信号,用于处理这些异常情况。
- **系统调试:**信号可以用于程序的调试,例如在程序运行时,可以向该进程发送 SIGUSR2 信号,用于打印程序的状态信息等。
-
-
信号的名字和编号
每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。
信号定义在signal.h头文件中,信号名都定义为正整数。
具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。
杀死进程
ps -aux|grep a.out//检索名字有a.out的进程 kill -9 16757(pid)// -9 是SIGKILL的代号,加上进程的pid是杀死这个进程
-
信号的处理
信号的处理有三种方法,分别是:忽略、捕捉和默认动作
- 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILL和SIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景
- 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
- 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。
具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。
-
信号函数——入门版
-
原型
//信号注册函数 #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); //信号发送函数 #include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);
-
捕捉信号
include <stdio.h> #include <signal.h> //typedef void (*sighandler_t)(int); //sighandler_t signal(int signum, sighandler_t handler); void handler(int signal_num) { printf("signal_num : %d\n",signal_num); switch(signal_num){ case 2: printf("SIGINT!\n"); break; case 9: printf("SIGKILL!\n"); break; case 10: printf("SIGUSR1!\n"); break; } printf("never quit!\n"); } int main() { signal(SIGINT,handler); signal(SIGKILL,handler); signal(SIGUSR1,handler); while(1); return 0; }
-
kill函数发送指令
#include <stdio.h> #include <signal.h> #include <sys/types.h> //int kill(pid_t pid, int sig); int main(int argc,char **argv) { int signum; pid_t pid; char cmd[128]; signum = atoi(argv[1]);//atoi:asc码转成int型 pid = atoi(argv[2]); printf("signum:%d,pid:%d\n",signum,pid); // kill(pid,signum);//kill函数发送指令 sprintf(cmd,"kill -%d %d",signum,pid);//组合字符串 system(cmd);//调用system发送指令 return 0; }
-
忽略指令
//SIG_IGN宏忽略指令,但是SIGKILL和SIGSTOP不能忽略 signal(SIGINT,SIG_IGN); signal(SIGKILL,SIG_IGN);
-
-
信号函数—高级版
-
我们已经成功完成了信号的收发,那么为什么会有高级版出现呢?其实之前的信号存在一个问题就是,虽然发送和接收到了信号,可是总感觉少些什么,既然都已经把信号发送过去了,为何不能再携带一些数据呢?
正是如此,我们需要另外的函数来通过信号传递的过程中,携带一些数据。
-
sigaction 的函数原型
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction { void (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作 void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用 sigset_t sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。 int sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据 }; //回调函数句柄sa_handler、sa_sigaction只能任选其一
-
sigqueue函数原型
#include <signal.h> int sigqueue(pid_t pid, int sig, const union sigval value); union sigval { int sival_int; void *sival_ptr; };
-
接收端
#include <signal.h> #include <stdio.h> //int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact); //void (*sa_sigaction)(int, siginfo_t *, void *); void handler(int signum, siginfo_t *info, void * context) { printf("SIGUSR1's signum = %d\n",signum); if(context != NULL){ printf("send pid = %d\n",info->si_pid); printf("get data = %d\n",info->si_int); printf("get data = %d\n",info->si_value.sival_int); } } int main() { printf("pid = %d\n",getpid()); struct sigaction action; action.sa_sigaction = handler; action.sa_flags = SA_SIGINFO; sigaction(SIGUSR1,&action,NULL); while(1); return 0; }
-
发送端
#include <signal.h> #include <stdio.h> //int sigqueue(pid_t pid, int sig, const union sigval value); int main(int argc,char **argv) { int signum; pid_t pid; signum = atoi(argv[1]); pid = atoi(argv[2]); union sigval value; value.sival_int = 100; sigqueue(pid,signum,value); printf("send done!\n"); return 0; }
-
七.信号量
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
-
特点
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
- 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
- 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
- 支持信号量组。
-
原型
最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。
Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
#include <sys/sem.h> // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1 int semget(key_t key, int num_sems, int sem_flags); // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1 int semop(int semid, struct sembuf semoparray[], size_t numops); // 控制信号量的相关信息 int semctl(int semid, int sem_num, int cmd, ...);
-
示例
-
创建一个信号量
union semun { int val; /* Value for SETVAL */ struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ unsigned short *array; /* Array for GETALL, SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */ };//控制信号量初始化的联合体 int semid; semid = semget(key,1,IPC_CREAT|0666);//创建信号量,“1”代表创建一个 union semun seminit;//定义一个初始化信号量的联合体 seminit.val = 1;//定义联合体中信号量的初始值 semctl(semid,0,SETVAL,seminit);//初始化第“0”个信号量的值为“1” //在semctl函数中的命令有多种,这里就说两个常用的: /*SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。 IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。*/
-
pv操作
信号量可以理解为一个信号灯,是一种公共资源,当有进程占用时,信号灯为红灯,表示被占用,其他进程不能使用,要在外面等待,此时信号量执行p操作。
当一个进程完成工作,需要将信号灯置为绿灯,表示其他进程可以使用,此时信号量执行v操作来释放资源。
信号量为二元信号量时:1为绿灯,0为红灯;p操作是将信号量减一操作,将信号量变为0,v操作是将信号量加一操作,将信号量变为1。
-
示例
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <stdio.h> #include <stdlib.h> union semun { int val; /* Value for SETVAL */ struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ unsigned short *array; /* Array for GETALL, SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO(Linux-specific) */ }; void sem_p(int semid)//p操作:占用资源,资源如果被占用,则等待 { struct sembuf buf; buf.sem_num = 0; buf.sem_op = -1;//信号量减一 buf.sem_flg = SEM_UNDO; semop(semid,&buf,1); printf("get key!\n"); } void sem_v(int semid)//v操作:释放资源 { struct sembuf buf; buf.sem_num = 0; buf.sem_op = 1;//信号量加一 buf.sem_flg = SEM_UNDO; semop(semid,&buf,1); printf("put the key back!\n"); } int main() { int semid; int pid; key_t key; key = ftok(".",2);//创建key semid = semget(key,1,IPC_CREAT|0666);//创建信号量 union semun seminit; seminit.val = 0; semctl(semid,0,SETVAL,seminit);//设置信号量初识值0 if((pid = fork()) == -1){ printf("fork error!\n"); exit(-1); } if(pid >0){//因为信号量初识值为0,被占用状态,所以需要等待释放后才能进行父进程操作 sem_p(semid); printf("this is father process!\n"); sem_v(semid); semctl(semid,0,IPC_RMID); } if(pid == 0){//父进程堵塞,子进程先运行 printf("this is child process!\n"); sem_v(semid);//释放资源后,父进程才能运行 } return 0; }
-