当从一个进程连接数据流到另外一个进程时,使用术语“管道”。通常是把一个进程的输出通过管道连接到另外一个进程的输入。Shell命令通过管道字符可以实现命令的连接:
cmd1 | cmd2 #cmd1的标准输入来自终端键盘 #cmd1的标准输出传递给cmd2,作为它的标准输入 #cmd2的标准输出连接到终端屏幕
Linux提供了类似的API,允许通过编程的方式,利用管道在两个程序之间传递数据。在两个程序之间进行数据传递的最简单方式是使用popen/pclose函数:
#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命令并获取其输出:
#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,该函数提供了对读写数据的更多控制:
#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在父子进程之间使用的例子:
#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()调用后,只需要将文件描述符传递给新进程就可以继续使用管道了,因为文件描述符本质上只是一个数字而已:
//将管道文件描述符保存到字符串中
sprintf(buffer, "%d", file_pipes[0]);
//传递给子进程
execl("command", buffer, (char *)0);
//读取参数为文件描述符
sscanf(argv[0], "%d", &file_descriptor);
//从文件描述符中读取数据
data_processed = read( file_descriptor, buffer, BUFSIZ );
关于管道的使用,应当注意:
通过管道连接两个进程,具有更加简洁的方法:
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 来创建命名管道:
mkfifo /tmp/my_fifo #尝试读取这个空白的命名管道 cat < /tmp/my_fifo #阻塞 #在另外一个终端尝试写入这个空白的命名管道 echo "Hello World" > /tmp/my_fifo #第一个终端读取到内容并输出在屏幕上
在程序中,可以使用以下两个调用:
#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 也会影响对管道的读写请求的处理方式:
//调用一直阻塞,除非另外一个进程以写方式打开同一命名管道 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还提供了更通用的,可以在不同进程之间进行通信的信号量机制,这些信号量接口都是针对成组的通用信号量进行操作,而不是针对一个二进制信号量。
#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 );
共享内存允许不相关的进程访问同一块逻辑内存。这是一种在进程之间传递数据的非常有效的方式,大部分实现都把共享内存安排为同一段物理内存。共享内存是进程地址空间中的一个特殊的范围。共享内存没有通过同步机制,因此需要使用其它同步机制来对共享内存的访问进行同步。
与共享内存相关的函数有:
#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:
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