Linux进程间通信
当从一个进程连接数据流到另外一个进程时,使用术语“管道”。通常是把一个进程的输出通过管道连接到另外一个进程的输入。Shell命令通过管道字符可以实现命令的连接:
1 2 3 4 5 |
cmd1 | cmd2 #cmd1的标准输入来自终端键盘 #cmd1的标准输出传递给cmd2,作为它的标准输入 #cmd2的标准输出连接到终端屏幕 |
Linux提供了类似的API,允许通过编程的方式,利用管道在两个程序之间传递数据。在两个程序之间进行数据传递的最简单方式是使用popen/pclose函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <stdio.h> /** * 允许将另外一个程序作为新进程启动,并可以传递数据或者接收数据 * @param command 需要运行的程序和参数 * @param open_mode 打开模式,必须是r或者w * 如果是r,被调用程序的输出可以被当前程序使用,通过返回的文件指针进行fread读取 * 如果是w,当前程序可以通过fwrite向被调用程序发送数据,后者可以在stdin上读取这些数据 */ FILE *popen( const char *command, const char *open_mode ); /** * 关闭文件指针,该函数只有在新进程结束后才会返回,否则会一直阻塞 */ int pclose( FILE *stream_to_close ); |
下面是一个简单的示例,执行uname命令并获取其输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> int main() { char buffer[BUFSIZ + 1]; int chars_read; memset( buffer, 0, sizeof ( buffer ) ); FILE *read_fp = popen( "uname -a", "r" ); //创建新进程并读取其标准输出 if ( read_fp != NULL ) { //像读取文件一样,将新进程的标准输出读取到缓冲区 chars_read = fread( buffer, sizeof(char), BUFSIZ, read_fp ); if ( chars_read > 0 ) { printf( "Output: %s\n", buffer ); } pclose( read_fp ); exit( EXIT_SUCCESS ); } exit( EXIT_FAILURE ); } |
在底层,Linux提供了pipe函数,通过该函数可以在两个进程之间传递数据,不需要启动Shell,该函数提供了对读写数据的更多控制:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <unistd.h> /** * 创建一个管道,该函数对入参数组填上两个新的文件描述符,然后返回0 * 返回的两个文件描述符通过一种特殊的方式连接:依据FIFO原则,写入 * file_descriptor[1]的数据,都可以从file_descriptor[0]中读取回来 * * @param file_descriptor 长度为2的文件描述符数组 * @return 如果成功返回0否则返回-1并设置errno: * EMFILE:进程使用的文件描述符过多 * ENFILE:系统的文件表已满 * EFAULT:文件描述符无效 */ int pipe( int file_descriptor[2] ); |
初看,pipe函数没有什么价值,但由于fork调用创建新进程时,默认原先打开的文件描述符仍然保持打开状态,因此,管道可以被父子进程共享,从而用来在其间进行数据传递,下面是pipe在父子进程之间使用的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> int main() { int data_processed; int file_pipes[2]; const char some_data[] = "MSG"; char buffer[BUFSIZ + 1]; pid_t fork_result; memset( buffer, '\0', sizeof ( buffer ) ); if ( pipe( file_pipes ) == 0 ) //创建一个共享的管道 { fork_result = fork(); //创建子进程 if ( fork_result == -1 ) exit( EXIT_FAILURE ); if ( fork_result == 0 ) { //这里是子进程,从文件描述符0中读取数据 data_processed = read( file_pipes[0], buffer, BUFSIZ ); printf( "Read %d bytes: %s\n", data_processed, buffer ); exit( EXIT_SUCCESS ); } else { //这里是父进程,向文件描述符1中写入数据 data_processed = write( file_pipes[1], some_data, strlen( some_data ) ); printf( "Wrote %d bytes\n", data_processed ); } } exit( EXIT_SUCCESS ); } |
上面的例子中,父子进程运行的是相同的程序,如果两个进程是完全不同的程序呢?通过exec()调用后,只需要将文件描述符传递给新进程就可以继续使用管道了,因为文件描述符本质上只是一个数字而已:
1 2 3 4 5 6 7 8 |
//将管道文件描述符保存到字符串中 sprintf(buffer, "%d", file_pipes[0]); //传递给子进程 execl("command", buffer, (char *)0); //读取参数为文件描述符 sscanf(argv[0], "%d", &file_descriptor); //从文件描述符中读取数据 data_processed = read( file_descriptor, buffer, BUFSIZ ); |
关于管道的使用,应当注意:
- 对于已经关闭写端的管道,对其指向read()调用不会阻塞,而会立即返回0。这与读取无效文件描述符不同,后者会返回-1
- 如果通过fork()调用使用管道,就会存在两个不同的文件描述符可以用来向管道写数据,一个在父进程中,一个在子进程中。只有在父子文件中均把针对管道的写描述符关闭,管道才认为是关闭的,对其进行read才会立即返回
通过管道连接两个进程,具有更加简洁的方法:
1 2 3 4 5 6 7 8 9 10 |
if ( fork_result == ( pid_t ) 0 ) // 子进程 { close( 0 ); //关闭标准输入 //复制管道读,根据dup的特点,它将复制为最小数值的文件描述符,即作为标准输入 dup( file_pipes[0] ); //关闭管道中两个文件描述符 close( file_pipes[0] ); close( file_pipes[1] ); execlp( "od", "od", "-c", ( char * ) 0 ); } |
要在两个不相关(不具备共同祖先)的进程之间传递数据,可以使用命名管道(Named pipe),命名管道又被称为FIFO文件。命名管道是一种特殊类型的文件,在文件系统中以文件名的形式存在,其行为却与前一节所述的管道类似。可以通过Shell命令 mknode 或者 mkfifo 来创建命名管道:
1 2 3 4 5 6 7 |
mkfifo /tmp/my_fifo #尝试读取这个空白的命名管道 cat < /tmp/my_fifo #阻塞 #在另外一个终端尝试写入这个空白的命名管道 echo "Hello World" > /tmp/my_fifo #第一个终端读取到内容并输出在屏幕上 |
在程序中,可以使用以下两个调用:
1 2 3 4 5 6 7 8 9 10 11 |
#include <sys/types.h> #include <sys/stat.h> int mkfifo( const char *filename, mode_t mode ); int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0); //创建命名管道的例子: int main() { int res = mkfifo( "/tmp/my_fifo", 0777 ); //尝试创建777权限的文件,当然需要受到用户掩码umask的制约 if ( res == 0 ) exit( EXIT_SUCCESS ); } |
通过open命令访问FIFO文件时,需要注意一个限制:不能以 O_RDWR 模式打开,因为FIFO只是为了单向的传递数据。如果以读写方式打开,进程将从管道中读取到自己写入的数据。如果确实需要双向的数据传输,可以使用一对命名管道。此外选项 O_NONBLOCK 也会影响对管道的读写请求的处理方式:
1 2 3 4 5 6 7 8 |
//调用一直阻塞,除非另外一个进程以写方式打开同一命名管道 open( const char *path, O_RDONLY ); //即使没有其它进程以写模式打开同一命名管道,调用也会成功并立即返回 open(const char *path, O_RDONLY | O_NONBLOCK); //调用一直阻塞,除非另外一个进程以读方式打开同一命名管道 open( const char *path, O_WRONLY ); //调用会立即返回,但是如果没有另外一个进程以读模式打开命名管道,调用将返回-1,并且FIFO也不会被打开 open(const char *path, O_WRONLY | O_NONBLOCK); |
与线程之间通信的信号量类似,Linux还提供了更通用的,可以在不同进程之间进行通信的信号量机制,这些信号量接口都是针对成组的通用信号量进行操作,而不是针对一个二进制信号量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#include <sys/sem.h> /** * 创建一个新的信号量,或者获取一个已经存在的信号量 * @param key 一个整数,不相关的进程可以通过同一key来访问同一信号量 * 特殊值IPC_PRIVATE表示创建一个只有当前进程才能看见的信号量 * @param num_sems 需要的信号量的数目,一般为1 * @param sem_flags 位或标记。低9位类似于文件权限;IPC_CREAT表示创建一个新的信号量; * IPC_EXCL | IPC_CREAT 表示确保获得一个新的、唯一的信号量,如果 * 信号量已经存在,会返回错误 * @return 成功返回正整数,表示信号量的唯一标识(sem_id);失败返回-1 */ int semget( key_t key, int num_sems, int sem_flags ); /** * 用于改变信号量的值,这是一个原子操作 * @param sem_id 信号量的唯一标识 * @param sem_ops 指向一个结构的指针 * @param num_sem_ops * */ struct sembuf { short sem_num; //信号量的数量,除非需要使用一组信号量,否则取值0 short sem_op; //信号量在一次操作中需要改变的值,可以使用非1值来改变信号量 //通常只会用到两个值:-1表示P操作,表示等待信号量可用;+1表示V操作,表示发送信号量可用的信息 short sem_flg; //通常设置为SEM_UNDO,它使得操作系统跟踪该信号量的修改情况 //如果进程没有释放持有的信号量就终止,操作系统会代为释放 }; int semop( int sem_id, struct sembuf *sem_ops, size_t num_sem_ops ); /** * 直接控制信号量信息 * @param sem_id 信号量的唯一标识 * @param sem_num 信号量的数量,除非需要使用一组信号量,否则取值0 * @param command 需要指向的操作 * @param semun 提供命令参数的联合体 */ union semun { int val; struct semid_ds *buf; unsigned short *array; }; int semctl( int sem_id, int sem_num, int command, union semun semun ); |
共享内存允许不相关的进程访问同一块逻辑内存。这是一种在进程之间传递数据的非常有效的方式,大部分实现都把共享内存安排为同一段物理内存。共享内存是进程地址空间中的一个特殊的范围。共享内存没有通过同步机制,因此需要使用其它同步机制来对共享内存的访问进行同步。
与共享内存相关的函数有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#include <cygwin/types.h> #include <stddef.h> //#include <sys/shm.h> /** * 创建共享内存 * @param key 共享内存段的命名,特殊键IPC_PRIVATE表示创建进程私有的共享内存 * @param size 共享内存的容量 * @param shmflg 位或,包含9个代表访问权限的位,IPC_CREAT用于创建一个新的共享内存 * 给此函数传递已经存在的key并不是错误,此时的IPC_CREAT会被忽略 * * @return 如果成功,返回一个正整数,作为共享内存的标识符;否则返回-1 */ int shmget( key_t key, size_t size, int shmflg ); /** * 第一次创建共享内存段时,它不能被任何进程访问。要启用对共享内存的访问,必须 * 将其连接到一个进程的地址空间中,这通过shmat函数完成 * @param shm_id 共享内存标识符 * @param shm_addr 连接到当前进程的地址位置,通常设置为空指针,表示让系统选择,否则硬件依赖性太高 * @param shmflg 标记位:SHM_RND与shm_addr联用,来控制连接地址;SHM_RDONLY 是共享内存对当前进程只读 * * @return 如果成功,返回指向共享内存第一个字节的指针;否则返回-1 */ void *shmat( int shm_id, const void *shm_addr, int shmflg ); /** * 将共享内存段从当前进程分离,该函数不会删除共享内存,只是使当前进程不再能访问它 * @param shm_addr shmat的返回值 */ int shmdt( const void *shm_addr ); /** * 控制共享内存 * @param shm_id 共享内存标识符 * @param cmd 采取的动作: * IPC_STAT 将shmid_ds中的数据设置为共享内存的当前关联值 * IPC_SET 如果有足够权限,则把shmid_ds中的值设置到共享内存 * IPC_RMID 删除共享内存段 * @param buf 指针,指向共享内存模式和访问权限的结构 */ struct shmid_ds { uid_t uid; uid_t gid; mode_t mode; }; int shmctl( int shm_id, int cmd, struct shmid_ds *buf ); |
消息队列类似于命名管道,但是不具有打开、关闭管道以及阻塞通信方面的复杂性。消息队列提供了一种从一个进程向另外一个进程发送数据块的机制,每个数据块被认为含有一个类型。下面是消息队列相关API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
MSGMAX //单个消息的最大字节数 MSGMNB //队列最大深度 #include <sys/msg.h> /** * 创建和访问一个消息队列 * @param key 队列的名字,IPC_PRIVATE用于创建私有队列 * @param msgflg 标记位,包含9个权限位,IPC_CREAT必须与这些位或才能创建新的队列 * @return 如果成功返回消息队列的标识符,否则返回-1 */ int msgget( key_t key, int msgflg ); /** * 把消息放入到队列中 * @param msqid 消息队列标识符 * @param msg_ptr 待发送消息的指针,目标应当是一个结构,且第一个成员变量是long型,用于表示消息类型 * @param msg_sz msg_ptr指向的消息的长度,不包括long型的消息类型的长度 * @param msgflg 控制当队列满或者队列消息到达系统范围限制时的行为,位或 * IPC_NOWAIT:立即返回-1,不发送消息;如果该标记被清除,则发送进程挂起直到队列有空闲 */ int msgsnd( int msqid, const void *msg_ptr, size_t msg_sz, int msgflg ); /** * 从队列里接收一个消息 * @param msqid 消息队列标识符 * @param msg_ptr 准备接收消息的指针 * @param msg_sz msg_ptr指向消息的长度,不包括long型消息类型的长度 * @param msgtype 消息类型,用于实现简单的优先级机制:如果小于0获取消息类型小于等于其绝对值的第一个消息 * 如果为0获取第一个消息;如果大于0获取对应类型的第一个消息 * @param msgflg 控制当队列为空时的行为,位或。IPC_NOWAIT类似msgsnd */ int msgrcv( int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg ); /** * 控制消息队列 * @param msqid 消息队列标识符 * @param cmd 命令。IPC_STAT,将消息队列关联值设置到buf;IPC_SET,将buf中的值设置到消息队列 * IPC_RMID,删除消息队列 * @param buf 存放命令参数 */ struct msqid_ds { uid_t uid; uid_t gid; mode_t mode; }; int msgctl( int msqid, int cmd, struct msqid_ds *buf ); |
Leave a Reply