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)插入进程的虚拟地址区域链表或树中

mmap的调用

	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:文件系统类型的权限

System V IPC guide

共享内存(share memory)

Linux进程间通信(System V) — 共享内存

消息队列(message queue)

System V消息队列

信号灯集(semaphore set)

进程间同步—system v ipc 对象信号灯集

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

Socket本地进程通信框架dbus

dbus通信与接口介绍

总结

  对于运行在不同机器上的Linux进程而言,进程间通信方式为网络通信。Linux网络通信的抽象实现为socket。从下往上看,应用层以下的网络协议、网络设备驱动都被封装纳入Linux文件系统体系,以socket及其相关API的形式暴露给应用程序。
  对于运行在同一机器的Linux系统下的进程而言,它们运行在独立的虚拟地址空间中,相互间的通信需要依赖操作系统提供的机制。一种是存储空间的共享,一种是信号的方式。信号使用简单但具有较大延迟性,适用于实时性要求不高、数据不大的情况。而共享内存适用于更广泛的应用场景,代价则是编码复杂度的提高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值