1、无名管道(Pipe)及有名管道(Named Pipe)
——管道是Linux中基于文件描述符的进程间通信方式之一,它把一个程序的输出直接连接到另一个程序的输入。
无名管道:用于具有亲缘关系进程间的通信。 (
不常用)
特点:
- 仅用于父子或者兄弟进程之间通信
- 半双工通信(有固定的读端和写端)
- 一种特殊文件(可使用read()、write()等函数,但不属于其它文件系统且仅存在于内存中)。
管道创建函数—— pipe(int fd[2])
函数传入值 fd[2]: 管道的两个文件描述符
返回值:成功 0 失败 -1
管道读写注意事项:
- 只有在管道的读端存在时,向管道写入数据才有意义。否则会收到内核传来的SIGPIPE信号。
- 向管道写入数据时, Linux将不保证写入的原子性,管道缓冲区有空闲区域,写进程就会试图向管道写入数据,如果读进程不读取管道缓冲区中的数据,那么写操作会一直阻塞。
- 父子进程在运行时,先后顺序不能保证。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <string.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)
{
/* Child process close read fd and suspend for 1s until father process have closed corresponding fd */
close(pipe_fd[1]);
sleep(DELAY_TIME);
/* Child process read pipe content */
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]); /* Close child process read fd*/
exit(0);
}
else if(pid > 0)
{
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); /* Collect info of child process when exit.*/
exit(0);
}
return 0;
}
标准流管道:基于文件流标准I/O模式的管道,主要用来创建一个连接到“另一个进程”的管道,另一个进程即指一个可以进行另一个操作的可执行文件。(
较常用)
常用函数:popen()、 pclose();
note: 使用popen()创建的管道必须使用标准I/O函数进行操作,不能使用前面的 read()、write()一类不带缓冲的函数。
#include <stdlib.h>
#include <unistd.h>
#include <string.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\n", buf);
}
pclose(fp);
exit(0);
}
有名管道:既具有管道具有的功能,也可以允许非无亲缘关系进程间的通信。
特点:
- 可用于互不相关的两个进程间通信
- 可以通过路径名来指出且在文件系统中可见
- FIFO严格遵循先进先出原则,对管道及FIFO的读总是从开始处返回数据,写则总是将数据添加到末尾,不支持如lseek()等文件定位操作。
以下包括两个采用阻塞式读写管道模式的程序fifo_read.c 和fifo_write.c,读管道程序中创建管道,读出由用户写入到管道中的内容;写管道程序要写的内容作为main()的参数传递给写管道程序。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#define MYFIFO "/tmp/myfifo" /* 有名管道文件名*/
#define MAX_BUFFER_SIZE PIPE_BUF /* 定义在 limit.h中 */
int main(int argc, char* argv[])
{
int fd;
char buff[MAX_BUFFER_SIZE];
int nread;
/* Judge whether the fifo is existed */
if(access(MYFIFO, F_OK) == -1)
{
if((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST))
{
printf("Cannot create fifo file\n");
}
}
/* Open the fifo in the blocked way */
fd = open(MYFIFO, O_RDONLY);
if(fd == -1)
{
printf("Open fifo file error\n");
exit(1);
}
while(1)
{
memset(buff, 0, sizeof(buff));
/* read string from named pipe */
if((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0)
{
printf("Read '%s' to FIFO\n", buff);
}
}
close(fd);
exit(0);
}
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <limits.h>
#include <fcntl.h>
#define MYFIFO "/tmp/myfifo" /* 有名管道文件名*/
#define MAX_BUFFER_SIZE PIPE_BUF /* 定义在 limits.h中 */
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);
fd = open(MYFIFO, O_WRONLY);
if(fd == -1)
{
printf("Open fifo file error\n");
exit(1);
}
/* Write string to named pipe */
if((nwrite = write(fd, buff, MAX_BUFFER_SIZE)) > 0)
{
printf("Write '%s' to FIFO\n", buff);
}
close(fd);
exit(0);
}
2、信号(Signal)
信号是在软件层次上对中断机制的一种模拟,它是一种比较复杂的通信方式,用于通知进程有某事件发生;一个进程收到一个信号与处理器收到一个终端请求的效果是一样的。信号是异步的,一个进程不必通过任何操作等待信号的到达,进程也不知道信号什么时候到达。信号可以直接进行用户进程和内核进程之间的交互,内核进程亦可利用信号来通知用户进程发生了哪些系统事件。
信号是进程间通信机制中唯一的异步通信机制,可以看作异步通知,用于通知接收信号的进程有哪些事件发生了。
信号的生命周期包括三个阶段:信号产生、信号在进程中注册和执行信号处理函数、信号在进程中注销。其中信号的产生、注册、注销是信号内部实现机制而不是信号的函数实现。 信号的处理包括信号的发送、捕捉及信号的处理。其各自对应的函数如下:
- 发送信号的函数:kill() raise()
- 捕捉信号的函数:alarm() pause()
- 处理信号的函数:sigal() sigaction()
信号事件发生来源:硬件来源(按键和硬件故障)、软件来源(发送信号的函数及非法运算操作等)
进程响应信号的三种方式:
- 忽略信号,但不包括SIGKILL、SIGSTOP。
- 捕捉信号, 定义信号处理函数,当信号事件发生时,执行相应的信号处理函数。
- 执行默认操作。
3、消息队列(Message Quene)
消息队列是消息的链接表,包括Posix消息队列和System V消息队列。它克服了管道和信号量通信方式中信息量有限的缺点,具有写权限的进程可以按照一定的规则向消息队列中添加新信息;具有消息队列读权限的进程可以从消息队列中读取消息。
4、共享内存(Shared Memory)
最有效的进程间通信方式,多个进程可以访问同一块内存空间,不同的进程可以看到对方进程中对共享内存中数据的更新。
note: 这种通信方式需要某种同步机制, 如互斥锁和信号量等。
5、信号量(Semaphore)
在多任务操作系统环境下,多个进程会同时运行,进程间会有可能协同完成同一个任务或者争夺系统资源进入竞争状态,即进程间的同步和互斥关系;信号量的出现主要作为进程之间及统一进程中不同线程之间的同步和互斥手段。它包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作(PV操作)。
PV原子操作定义如下:
P操作: 如果有可用的资源(信号量值 > 0),则占用一个资源(给信号量值减1,进入临界区代码);如果没有可用的资源(信号量值=0),则被阻塞直到系统将资源分配给该进程(进入等待队列,一直等到资源轮到该进程);
V操作: 如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程;如果没有进程等待它,则释放一个资源(给信号量值加1);
使用信号量访问临界区的常用伪代码如下:
{ /* 设R为某种资源,S为某种资源的信号量 */
INIT_VAL(S); /* 对信号量S进行初始化 */ 非临界区; P(S); /* 进行P操作 */ 临界区(使用资源R) /* 只有有限个进程被允许进入该区 */ V(S); /* 进行V操作 */ 非临界区; }
使用信号量的基本步骤及涉及的函数:
创建信号量及获得在系统中已经存在的信号量,不同进程可以通过使用同一个信号量键值来获得同一个信号量 —— 函数semget();
初始化信号量(二维信号量一般初始化为1) —— 函数semctl()中的SETVAL操作;
进行信号量的PV操作,实现进程间同步和互斥的核心工作部分 —— 函数semop();
如果不需要信号量,则从系统中删除掉它(已删除的信号不可在程序中再次操作) —— 函数setctl()中IPC_RMID;
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h>#include <fcntl.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 */
};
/* Function:Initialze the semaphore */
int init_sem(int sem_id, int init_value)
{
union semun sem_union;
sem_union.val = init_value;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1)
{
perror("Initialize semaphore");
return -1;
}
return 0;
}
/* Function:Delete the semaphore from system */
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;
}
}
/* Function: P operation */
int sem_p(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num = 0; /* Single semaphore should be set with 0 */
sem_b.sem_op = -1; /* P operation */
sem_b.sem_flg = SEM_UNDO; /* Auto free semaphore that has left in system */
if(semop(sem_id, &sem_b, 1) == -1)
{
perror("P operation");
return -1;
}
return 0;
}
/* Function: V operation */
int sem_v(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num = 0; /* Single semaphore should be set with 0 */
sem_b.sem_op = 1; /* V operation */
sem_b.sem_flg = SEM_UNDO; /* Auto free semaphore that has left in system */
if(semop(sem_id, &sem_b, 1) == -1)
{
perror("V operation");
return -1;
}
return 0;
}
利用信号量控制进程同步的实例程序—— sem_fork.c:
说明:
信号量操作增加之前进程执行顺序:父进程 — 子进程
信号量操作增加之后进程执行顺序:子进程 — 父进程
#include "sem_com.h"
#define DELAY_TIME 3
int main()
{
pid_t result;
int sem_id;
sem_id = semget(ftok(".", 'a'), 1, 0666|IPC_CREAT);
init_sem(sem_id, 0);
/* Call the fork() */
result = fork();
if(result == -1)
{
perror("fork()");
}
else if(result == 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
{
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);
}
6、套接字(Socket)
应用非常广泛的一种进程间通信机制,可以用于网络中不同机器之间的进程间通信。