Linux进程间的通信
早期UNIX进程通信方式
无名管道(pipe)
管道概述
管道是一种最基本古老而的IPC机制,作用于有血缘关系的进程之间,完成数据传递。
调用pipe系统函数即可创建一个管道。有如下特质:
1)其本质是一个伪文件(实为内核缓冲区) ,不属于某个文件系统,其只存在于内存中。
2)由两个文件描述符引用,一个表示读端,一个表示写端。
3)规定数据从管道的写端流入管道,从读端流出,先入先出。
4)管道所传送的数据是无格式的。
5)当两个进程都终结的时候,管道也自动消失。
管道的原理
管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
1)一般默认缓冲区大小为4K,不同系统不一定相同
2)实际操作过程中缓冲区会根据进程的数据压力做适当调整
管道的局限性
1)数据一旦被读走,便不在管道中存在,不可反复读取。
2)管道采用半双工通信方式。因此,数据只能在一个方向上流动。
3)只能在有公共祖先的进程间使用管道。
管道创建
pipe函数:
#include <unistd.h>
int pipe(int filedes[2]);
---功能:经由参数filedes返回两个文件描述符
---参数:
---filedes为int型数组的首地址,其存放了管道的文件描述符fd[0]、fd[1]。
---filedes[0]为读而打开,filedes[1]为写而打开管道,filedes[0]的输出是filedes[1]的输入。
---返回值:
---成功:返回 0
---失败:返回-1
使用过程:
1)父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。
2)父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
3)父进程关闭管道读端,子进程关闭管道写端。
父进程可以向管道中写入数据,子进程将管道中的数据读出。
由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
无名管道示例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main( int argc, const char* argv[] ){
int pipe_id[2];
pid_t child_pid;
int return_val;
char write_buff[1024];
char read_buff[1024];
int return_n_write;
int return_n_read;
/*1、创建管道*/
return_val = pipe(pipe_id);
if( return_val == -1 ){
perror( "pipe create\n" );
return -1;
}
child_pid = fork();
if( child_pid == -1 ){
perror( "process create\n" );
}
else if( child_pid == 0 )
{
/*子进程:写,关闭读端*/
close( pipe_id[0] );
while( 1 ){/*子进程开始向管道写数据*/
fgets( write_buff, sizeof(write_buff), stdin );
return_n_write = write( pipe_id[1], write_buff, 1024 );
if( return_n_write == -1 ){
perror( "write error\n" );
return -1;
}
}
}
else if( (child_pid > 0) )
{
/*主进程,读取管道内容*/
close( pipe_id[1] );
while(1)
{
return_n_read = read( pipe_id[0], read_buff, 1024 );
if( return_n_read == -1 ){
printf("read from write buff error\n");
return -1;
}
printf( "Read from pipe: %s\n",read_buff );
/*判断输入的是不是quit,如果是的话,结束子进程*/
if( !( strncmp( write_buff, "quit", 4 )) )
break;
}
}
waitpid( child_pid, NULL, 0 );
return 0;
}
管道的读写行为
1)默认用read函数从管道中读数据是阻塞的。
2)调用write函数向管道里写数据,当缓冲区已满时write也会阻塞。
3)通信过程中,读端口全部关闭后,写进程向管道内写数据时,写进程会(收到SIGPIPE信号)退出。
4)编程时可通过fcntl函数设置文件的阻塞特性。
设置为阻塞:
fcntl(fd, F_SETFL, 0);
设置为非阻塞:
fcntl(fd, F_SETFL, O_NONBLOCK);
管道缓冲区大小
*方式1
可以使用ulimit –a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。通常为:
pipe size (512 bytes, -p) 8 // 8个扇区,每个扇区512字节
*方式2
也可以使用fpathconf函数,借助参数 选项来查看。
引入的头文件:include <unistd.h>
函数原型:long fpathconf(int fd, int name);
函数说明:该函数可以通过name参数查看不同的属性值
参数:name
_PC_PIPE_BUF -- 查看管道缓冲区大小
_PC_NAME_MAX -- 文件名字字节数的上限
返回值:
成功:根据name返回的值的意义也不同。
失败:-1,设置errno
有名管道(fifo)
FIFO概述
FIFO常被称为命名管道,以区分管道(pipe)。
管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据。
FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。
各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。
其特点如下:
1)半双工,数据在同一时刻只能在一个方向上流动。
2)写入FIFO中的数据遵循先入先出的规则。
3)FIFO所传送的数据是无格式的。
4)FIFO在文件系统中作为一个特殊的文件而存在,但FIFO中的内容却存放在内存中。
5)管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。
6)从FIFO读数据是一次性操作,数据一旦被读,它就从FIFO中被抛弃,释放空间。
7)当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用。
8)FIFO有名字,不相关的进程可以通过打开命名管道进行通信
FIFO的使用
*方式1 – 使用命令
mkfifo 管道名
*方式2 - 使用库函数
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo( const char *pathname, mode_t mode);
参数:
pathname:FIFO的路径名+文件名。
mode:mode_t类型的权限描述符。
返回值:
成功:返回 0
失败:如果文件已经存在,则会出错且返回-1。
说明:
使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。
FIFO严格遵循先进先出(first in first out),它们不支持诸如lseek()等文件定位操作。
FIFO的行为模式
*不指定O_NONBLOCK(即open没有位或O_NONBLOCK):
1)open以只读方式打开FIFO时,要阻塞到某个进程为写而打开此FIFO
2)open以只写方式打开FIFO时,要阻塞到某个进程为读而打开此FIFO。
3)open以只读、只写方式打开FIFO时会阻塞,调用read函数从FIFO里读数据时read也会阻塞。
4)通信过程中若写进程先退出了,则调用read函数从FIFO里读数据时不阻塞;
若写进程又重新运行,则调用read函数从FIFO里读数据时又恢复阻塞。
5)通信过程中,读进程退出后,写进程向命名管道内写数据时,写进程也会(收到SIGPIPE信号)退出。
6)调用write函数向FIFO里写数据,当缓冲区已满时write也会阻塞
*指定O_NONBLOCK(即open位或O_NONBLOCK)
1)先以只读方式打开:如果没有进程已经为写而打开一个FIFO, 只读open成功,并且open不阻塞。
2)先以只写方式打开:如果没有进程已经为读而打开一个FIFO,只写open将出错返回-1。
3)read、write读写命名管道中读数据时不阻塞。
4)通信过程中,读进程退出后,写进程向命名管道内写数据时,写进程也会(收到SIGPIPE信号)退出。
**open函数以可读可写方式打开FIFO文件时的特点:
1)open不阻塞。
2)调用read函数从FIFO里读数据时read会阻塞。
3)调用write函数向FIFO里写数据,当缓冲区已满时write也会阻塞。
FIFO使用示例:
/*数据生产者*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024 * 1024 *10 )
int main()
{
int pipe_fd;
int res;
int open_mode = O_WRONLY;
int bytes_sent = 0;
char buffer[BUFFER_SIZE + 1];
if( -1 == access(FIFO_NAME, F_OK) ) {
res = mkfifo( FIFO_NAME, 0777 ); //创建一个FIFO
if( 0 != res ) {
fprintf( stderr, "Could not create fifo %s\n", FIFO_NAME );
exit( EXIT_FAILURE );
}
}
printf("Process %d opening FIFO O_WRONLY\n", getpid() );
pipe_fd = open( FIFO_NAME, open_mode );
printf("Process %d result %d\n", getpid(), pipe_fd);
if(-1 != pipe_fd ) {
while( bytes_sent < TEN_MEG ) {
res = write(pipe_fd, buffer, BUFFER_SIZE );
if( -1 == res ) {
fprintf(stderr, "Write error on pipe\n");
exit( EXIT_FAILURE );
}
bytes_sent += res;
}
close( pipe_fd );
}
else
exit( EXIT_FAILURE );
printf("Process %d finished\n", getpid() );
exit( EXIT_SUCCESS );
}
--------------------------------------------------
/*数据消费者*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF
int main()
{
int pipe_fd;
int res;
int open_mode = O_RDONLY;
int bytes_read = 0;
char buffer[BUFFER_SIZE + 1];
memset( buffer, '\0', sizeof(buffer) );
printf("Process %d opening FIFO O_RDONLY\n", getpid() );
pipe_fd = open( FIFO_NAME, open_mode );
printf("Process %d result %d\n", getpid(), pipe_fd);
if( -1 != pipe_fd ) {
do {
res = read( pipe_fd, buffer, BUFFER_SIZE );
if( -1 == res ) {
fprintf( stderr, "Write error on pipe\n");
exit( EXIT_FAILURE );
}
bytes_read += res;
}while( res>0 );
close( pipe_fd );
}
else
exit( EXIT_FAILURE );
printf("Process %d finished, %d bytes read\n", getpid(), bytes_read );
exit( EXIT_SUCCESS );
}
信号(signal)
信号概述
信号是信息的载体,Linux/UNIX 环境下,古老、经典的通信方式, 现下依然是主要的通信手段。
Unix早期版本就提供了信号机制,但不可靠,信号可能丢失。Berkeley 和 AT&T都对信号模型做了更改,增加了可靠信号机制。但彼此不兼容。
POSIX.1对可靠信号例程进行了标准化。
信号发生时,程序收到信号后,要暂停运行,去处理信号,处理完毕再继续执行。这与硬件中断类似——异步模式。
但信号是软件层面上实现的中断,早期常被称为“软中断”。
信号的实现手段导致信号有很强的延时性,但对于用户来说,时间非常短,不易察觉。
linux下的信号
可以使用kill –l命令查看当前系统可使用的信号有哪些。也可通过man 7 signal查看帮助文档获取。
不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。
名字上区别不大。而前32个名字各不相同。
在标准信号中,有一些信号是有三个“Value”,第一个值通常对alpha和sparc架构有效,中间值针对x86、arm和其他架构,最后一个应用于mips架构。
一个‘-’表示在对应架构上尚未定义该信号。
不同的操作系统定义了不同的系统信号。因此有些信号出现在Unix系统内,也出现在Linux中,而有的信号出现在FreeBSD或Mac OS中却没有出现在Linux下。
默认动作:
Term:终止进程
Ign: 忽略信号 (默认即时对该种信号忽略操作)
Core:终止进程,生成Core文件。(查验死亡原因,用于gdb调试)
Stop:停止(暂停)进程
Cont:继续运行进程
注意通过man 7 signal命令查看帮助文档,其中可看到: The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
这里特别强调了9) SIGKILL 和19) SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
信号的产生
1)按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\
** Ctrl + c → 2) SIGINT(终止/中断) "INT" ----Interrupt
** Ctrl + \ → 3) SIGQUIT(退出)
** Ctrl + z → 20) SIGTSTP(暂停/停止) "T" ----Terminal 终端
2) 硬件异常产生,如:非法访问内存(段错误)、除0、内存对齐出错(总线错误)
** 总线错误 → 7) SIGBUS
** 除0操作 → 8) SIGFPE (浮点数例外) "F" -----float 浮点数。
** 非法访问内存 → 11) SIGSEGV (段错误)
3)软件条件产生,如:定时器alarm
4)系统调用产生,如:kill、raise、abort
5)命令产生,如:kill命令
kill函数/命令
kill描述:给指定进程发送指定信号(不一定杀死)
kill命令:kill -SIGKILL 进程ID
kill函数原型:int kill(pid_t pid, int sig); // man 2, #include <signal.h>
函数返回值:
成功:0;
失败:-1 (ID非法,信号非法,普通用户杀init进程等权级问题),设置errno
函数参数:
sig信号参数:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
pid参数:
pid > 0: 发送信号给指定的进程。
pid = 0: 发送信号给 与调用kill函数进程属于同一进程组的所有进程。
pid < -1: 取|pid|发给对应进程组。
pid = -1:发送给进程有权限发送的系统中所有进程。
*进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,
每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。
*权限保护:super用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。
同样,普通用户也不能向其他普通用户发送信号,终止其进程。 只能向自己创建的进程发送信号。
普通用户基本规则是:发送者实际或有效用户ID == 接收者实际或有效用户ID
sigqueue函数
函数描述:与kill对应,用于发送信号,可传参
函数原型:int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval
{
int sival_int;
void *sival_ptr;
};
函数返回值:成功:0;失败:-1,设置errno
raise函数
函数描述:给当前进程发送指定信号(自己给自己发)
函数原型:int raise(int sig); // man 3, #include <signal.h>
函数返回值:成功:0,失败非0值
*函数拓展:raise(signo) == kill(getpid(), signo);
abort函数
函数描述:给自己发送异常终止信号 6) SIGABRT,并产生core文件
函数原型:void abort(void); // man 3, #include <stdlib.h>
*函数拓展:abort() == kill(getpid(), SIGABRT);
alarm函数
函数原型:unsigned int alarm(unsigned int seconds); //man 3, #include <unistd.h>
函数描述:设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。
每个进程都有且只有唯一的一个定时器。
函数返回值:返回0或剩余的秒数,无失败。例如:
常用操作:alarm(0),取消定时器,返回旧闹钟余下秒数。
**自然定时法:就绪、运行、挂起(阻塞、暂停)、终止、僵尸...无论进程处于何种状态,alarm都计时。
setitimer函数
函数原型:
int setitimer(int which,
const struct itimerval *new_value,
struct itimerval *old_value
); // man 3, #include <sys/time.h>
函数描述:设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
函数返回值:成功:0;失败:-1,设置errno
函数参数:
which:指定定时方式
--自然定时:ITIMER_REAL → 14)SIGALRM计算自然时间
--虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM 只计算进程占用cpu的时间
--运行时计时(用户+内核):ITIMER_PROF → 27)SIGPROF计算占用cpu及执行系统调用的时间
new_value:struct itimerval, 负责设定timeout时间。
itimerval.it_value: 设定第一次执行function所延迟的秒数
itimerval.it_interval: 设定以后每几秒执行function
struct itimerval {
struct timerval it_interval; // 闹钟触发周期
struct timerval it_value; // 闹钟触发时间
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
}
old_value: 存放旧的timeout值,一般指定为NULL
信号的状态
1)递达:递送并且到达进程。
2)未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
信号集:
Linux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,
还包含了信号相关的信息,主要指阻塞信号集和未决信号集:
*阻塞信号集(信号屏蔽字)
将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(处理发生在解除屏蔽后)
*未决信号集:
信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
操作信号集
信号集是一个能表示多个信号的数据类型,sigset_t set,set即一个信号集。
int sigemptyset(sigset_t *set);
** 函数说明:将某个信号集清0
** 函数返回值:成功:0;失败:-1,设置errno
int sigfillset(sigset_t *set);
** 函数说明:将某个信号集置1
** 函数返回值:成功:0;失败:-1,设置errno
int sigaddset(sigset_t *set, int signum);
** 函数说明:将某个信号加入信号集合中
** 函数返回值:成功:0;失败:-1,设置errno
int sigdelset(sigset_t *set, int signum);
** 函数说明:将某信号从信号清出信号集
** 函数返回值:成功:0;失败:-1,设置errno
int sigismember(const sigset_t *set, int signum);
** 函数说明:判断某个信号是否在信号集中
** 函数返回值:在:1;不在:0;出错:-1,设置errno
------------------------------------------------------------------------------------
除sigismember外,其余操作函数中的set均为传出参数。
sigset_t类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
------------------------------------------------------------------------------------
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
** 函数说明:用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程控制块中的信号屏蔽字(阻塞信号集)。
严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃处理。
** 函数参数:
-- how参数取值:假设当前的信号屏蔽字为mask
SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set
SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。
相当于mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
-- set参数取值:传入参数,是一个自定义信号集合。由参数how来指示如何修改当前信号屏蔽字。
-- oldset参数取值:传出参数,保存旧的信号屏蔽字。
** 函数返回值:成功:0;失败:-1,设置errno
int sigpending(sigset_t *set);
** 函数说明:读取当前进程的未决信号集
** 函数返回值:成功:0;失败:-1,设置errno
信号的处理
1)执行默认动作
2)忽略(丢弃)
3)捕捉(调用户处理函数)
信号捕捉:
signal函数
注册一个信号捕捉函数:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数返回值:成功:返回函数指针;失败:返回SIG_ERR,设置errno
该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。
因此应该尽量避免使用它,取而代之使用sigaction函数。
sigaction函数
修改信号处理动作(通常在Linux用其来注册一个信号的捕捉函数)
int sigaction( int signum,
const struct sigaction *act,
struct sigaction *oldact
);
函数返回值:成功:0;失败:-1,设置errno
函数参数:
sinnum:捕捉的信号
act:传入参数,新的处理方式。
oldact:传出参数,旧的处理方式。
struct sigaction结构体:
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); // 信号处理函数
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_restorer:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略或SIG_DFL表执行默认动作
sa_mask: 用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,
因此在信号处理函数执行期间这个信号不会再度发生。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
sa_flags:通常设置为0,表使用默认属性。
信号捕捉特性:
1)进程正常运行时,默认PCB中有一个信号屏蔽字,假定为A,它决定了进程自动屏蔽哪些信号。
当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由A来指定。
而是用sa_mask来指定。调用完信号处理函数,再恢复为A。
2)XXX信号捕捉函数执行期间,XXX信号自动被屏蔽。
3)阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)
共享存储映射
概述
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。
与此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用存储映射这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
mmap函数
函数原型:
void *mmap(
void *adrr, // 映射区首地址,传NULL
size_t length, // 映射区的大小
int prot, // 映射区权限
int flags, // 标志位参数
int fd, // 文件描述符
off_t offset // 映射文件的偏移量
);
函数描述:一个文件或者其它对象映射进内存。
函数返回值:
成功:返回创建的映射区首地址;
失败:MAP_FAILED宏
参数:
addr: 指定映射的起始地址, 通常设为NULL, 由系统指定
length:映射到内存的文件长度
prot: 映射区的保护方式, 最常用的:
读:PROT_READ
写:PROT_WRITE
读写:PROT_READ | PROT_WRITE
flags: 映射区的特性, 可以是
MAP_SHARED: 写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享。
MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。
fd:由open返回的文件描述符, 代表要映射的文件。
offset:以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射。
munmap函数
函数描述:同malloc函数申请内存空间类似的,mmap建立的映射区在使用结束后也应调用类似free的函数来释放。
函数原型:int munmap(void *addr, size_t length);
函数参数:
addr -- 使用mmap函数创建的映射区的首地址
length -- 映射区的大小
函数返回值:成功:0; 失败:-1
注意事项
1)创建映射区的过程中,隐含着一次对映射文件的读操作。
2)当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。
而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
3)映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。
4)特别注意,当映射文件大小为0时,不能创建映射区。
所以:用于映射的文件必须要有实际大小!!
mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
5)munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
6)如果文件偏移量必须为4K的整数倍
7)mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
匿名映射
无需依赖一个文件即可创建映射区。需要借助标志位参数flags来指定。
使用MAP_ANONYMOUS (或MAP_ANON), 如:
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS, -1, 0);
MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。
在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
① fd = open("/dev/zero", O_RDWR);
② p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
mmap原理
linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。
各个vm_area_struct结构使用链表或者树形结构链接。
vm_area_struct结构中包含区域起始和终止地址以及其他相关信息,
同时也包含一个vm_ops指针,其内部可引出所有针对这个区域可以使用的系统调用函数。
mmap函数就是要创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。
mmap内存映射的实现过程如下:
1)进程在用户空间调用库函数mmap
2)在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
3)为此虚拟区分配一个vm_area_struct结构并初始化
4)将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
5)为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,
通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),
每个文件结构体维护着和这个已打开文件相关各项信息。
6)通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap(对于普通文件来说,linux有默认实现,
对于驱动文件来说,将会调用由驱动编写者设置的驱动程序中file_operations结构体.mmap变量所对应的回调函数),
其原型为:int mmap(struct file *filp, struct vm_area_struct *vma)。
7)内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
8)通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。
此时,这片虚拟地址并没有任何数据关联到主存中。
9)进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。
因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
10)缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
11)调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,
如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
12)之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,
一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
*注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,
可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。
使用mmap读写文件性能分析
read/write系统调用:进程调用read或是write后会陷入内核,内核首先把文件读入自己的内核空间缓冲区,读完之后进程在内核回归用户态,内核把读入内核内存的数据再copy进入进程的用户态内存空间。写亦如此。两次拷贝。
mmap内存映射函数:它把文件内容映射到一段虚拟内存上, 通过对这段内存的读取和修改,通过缺页机制,实现对文件的读取和修改。省去了内核空间到用户空间的拷贝。
当今硬件技术的发展,使得内存拷贝消耗的时间已经极大降低了。但mmap()的开销在于一次 pagefault,这个开销相比而言已经更高了,而且 pagefault 的处理任务现在比以前还更多了;而且,mmap之后,再有读操作不会经过系统调用,在 LRU 比较最近使用的页的时候不占优势。于是,普通读情况下,read() 通常会比 mmap() 来得更快。
使用mmap进行非亲缘进程间通信
进程1写:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<string.h>
struct STU
{
int age;
char name[20];
char sex;
};
int main(int argc,char *argv[])
{
if(argc != 2)
{
printf("./a,out file");
exit(1);
}
struct STU student = {10,"xiaoming",'m'};
int fd = open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd < 0)
{
perror("open");
exit(2);
}
ftruncate(fd,sizeof(struct STU));
struct STU *p = (struct STU*)mmap(NULL,sizeof(struct \
STU),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(p == MAP_FAILED)
{
perror("mmap");
exit(3);
}
close(fd);
while(1)
{
memcpy(p,&student,sizeof(student));
student.age++;
sleep(1);
}
int ret = munmap(p,sizeof(student));
if(ret < 0)
{
perror("mmumap");
exit(4);
}
return 0;
}
-----------------------------------------------
进程2读:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h>
struct STU
{
int age;
char name[20];
char sex;
};
int main(int argc,char *argv[])
{
if(argc != 2)
{
printf("./a,out file");
exit(1);
}
int fd = open(argv[1],O_RDONLY,0644);
if(fd < 0)
{
perror("open");
exit(2);
}
struct STU student;
struct STU *p = (struct STU*)mmap(NULL,sizeof(struct \
STU),PROT_READ,MAP_SHARED,fd,0);
if(p == MAP_FAILED)
{
perror("mmap");
exit(3);
}
close(fd);
int i = 0;
while(1)
{
printf("id = %d\tname = %s\t%c\n",p->age,p->name,p->sex);
sleep(2);
}
int ret = munmap(p,sizeof(student));
if(ret < 0)
{
perror("mmumap");
exit(4);
}
return 0;
}
System V IPC
概述
System V IPC最初是在一个名为 “Columbus Unix” 的开发版Unix变种中引入的,之后在AT&T的System III中采用。现在在大部分Unix系统 (包括 Linux) 中都可以找到。
IPC资源包含信号量、消息队列和共享内存三种。IPC的数据结构是在进程请求IPC资源时动态创建的。每个IPC资源都是持久的:除非被进程显式地释放,否则永远驻留在内存中(直到系统关闭)。IPC资源可以由任一进程使用,以实现进程间通信。
由于一个进程可能需要同类型的多个IPC资源,因此每个新资源都是使用一个32位的IPC关键字来标识的。IPC标识符由内核分配给IPC资源,在系统内部是唯一的,而IPC关键字可以由程序员自由地选择。当两个或者更多的进程要通过一个IPC资源进行通信时,这些进程都要引用该资源的IPC标识符。
这几个System V IPC对象的访问权限是对象创建者通过系统调用设定的,访问这些System V IPC对象首先要经过权限检查。
在Linux中,描叙这几种System V IPC对象的数据结构中都包含一个ipc_perm结构,这个结构中包含了对象的所有者,创建者和进程的用户ID,组ID,还包括对象的访问权限和IPC对象关键字(键值)。关键字用来确定System V IPC 对象的引用ID的位置。Linux中支持两种关键字:public 和private。若是public,那么系统中的所有进程都可以找到System VPIC对象的应用ID;若是private,只有对象的创建者和同组有权查看System V IPC对象的引用ID.。
System V IPC对象的统一属性,总结如下:
1)键key :一个由用户提供的整数,用来标志这个资源的实例
2)创建者creator:创建这个资源的进程的用户ID(UID),组ID(GID)
3)所有者owner:资源的所有者的UID和GID
4)权限permisss:文件系统类型的权限
共享内存(share memory)
消息队列(message queue)
信号灯集(semaphore set)
BSD UNIX
套接字(socket)
概述
“socket”一词在组网领域的首次使用是在1970年2月12日发布的文献IETF RFC33中,撰写者为Stephen Carr、Steve Crocker和Vint Cerf。根据美国计算机历史博物馆的记载,Croker写道:“命名空间的元素都可称为套接字接口。一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。”计算机历史博物馆补充道:“这比BSD的套接字接口定义早了大约12年。”
socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。借助这些操作,实现本地进程或网络进程之间的通信。
Socket详解
Socket本地进程通信框架dbus
总结
对于运行在不同机器上的Linux进程而言,进程间通信方式为网络通信。Linux网络通信的抽象实现为socket。从下往上看,应用层以下的网络协议、网络设备驱动都被封装纳入Linux文件系统体系,以socket及其相关API的形式暴露给应用程序。
对于运行在同一机器的Linux系统下的进程而言,它们运行在独立的虚拟地址空间中,相互间的通信需要依赖操作系统提供的机制。一种是存储空间的共享,一种是信号的方式。信号使用简单但具有较大延迟性,适用于实时性要求不高、数据不大的情况。而共享内存适用于更广泛的应用场景,代价则是编码复杂度的提高。