嵌入式系统及应用Linux学习笔记(六)---进程通信

进程的通信

  • fork新的进程完全复制父进程的数据,父子进程直接独立,没有影响 进程与进程间的数据空间是相互独立的
  • 一个进程的代码段只能存取其自己的数据,而不能存取另一个进程的数据
  • 写一个程序测试:用全局变量能否在父子进程直接传递数据?
    • 定义全局变量
    • 子进程对全局变量操作
    • 父进程对全局

进程通信的目的

  • A、数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几M字节之间
  • B、共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
  • C、通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • D、资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
  • E、进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程通信方式概览

  • 早期UNIX进程间通信
    • 管道、FIFO、信号
    • 基于System V进程间通信
  • System V, 曾经也被称为AT&T SystemV,是Unix操作系统众多版本中的一支
    • System V消息队列、System V信号量、System V共享内存
  • 基于POSIX进程间通信
    • posix消息队列、posix信号量、posix共享内存
  • 基于Socket进程间通信

POSIX & System V

  • POSIX(Portable Operating System Interface)表示可移植操作系统接口。电气和电子工程师协会(Institute of Electrical and Electronics Engineers,IEEE)最初开发POSIX 标准,是为了提高UNIX 环境下应用程序的可移植性。然而,POSIX 并不局限于UNIX,许多其它的操作系统,例如DEC Open VMS 和Microsoft
    Windows,都支持POSIX 标准。
  • System V, 曾经也被称为AT&T SystemV,是Unix操作系统众多版本中的一支。

进程间的通信手段

  • 无名管道(Pipe)及有名管道(named pipe):无名管道仅用于具有亲缘关系进程间的通信;而有名管道除具有管道所具有的功能外,还允许无亲缘关系进程间的通信。
  • 共享内存:使得多个进程可以访问同一块内存空间,是最快的IPC形式,是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
  • 信号(Signal):用于通知接收进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身。
  • 消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列systemV消息队列。有足够权限的进程可向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 套接口(Socket):用于不同机器之间的进程间通信。
  • 信号量(Semaphore):信号量是用于解决进程的同步和相关资源抢占而设计的。即提供对共享资源的访问控制机制。

进程间通信(IPC)的方法

  • 管道(文件)
    • 基本原理:通过读写文件进行通信
    • 管道:特殊的文件,用来通信
      • 特殊之处:内存中
    • 无名管道(pipe):没有名字,使用文件描述符(整数)表示
      • int pipe(int files[2]); 参数为一个整型数组
      • files[0]读,files[1]写
    • 有名管道(FIFO)
      • 有路径,inode节点在磁盘,但并没实际分配磁盘空间
        • 方式一:mkfifo("/tmp/myfifo", 0644 ); 程序中使用函数调用
        • 方式二:mknod myfifo p 命令行(p参数代表FIFO文件)
        • 方式三:命令 mkfifo -m 0644 /tmp/cmd_pipe

管道通信

  • 管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。
  • 数据被一个进程读出后,将被从管道中删除,其它读进程将不能再读到这些数据。管道提供了简单的流控制机制,进程试图读空管道时,进程将阻塞。同样,管道已经满时,进程再试图向管道写入数据,进程将阻塞。

无名管道示例

无名管道由pipe( )函数创建:int pipe(int fd[2]);当一个管道建立时它会创建两个文件描述符:fd[0] 用于读管道,fd[1] 用于写管道。
嵌入式系统及应用Linux学习笔记(六)---进程通信_第1张图片
无名管道用于不同进程间通信。通常先创建一个管道,再通过fork函数创建一个子进程,该子进程会继承父进程所创建的管道。

嵌入式系统及应用Linux学习笔记(六)---进程通信_第2张图片

必须在系统调用fork()前调用pipe(),否则子进程将不会继承文件描述符。

写有名管道示例

int main(int argc, char* argv[])
{
	int fd,nwrite;
	char w_buf[100];
	if(argc==1)
	{
                  printf("Please send something\n");
	    exit(-1);	
               }
	fd= open("myfifo",O_RDWR|O_NONBLOCK);                
	if(fd==-1)
               {
                   perror("open file error");exit(-1);
               }
              //用命令行参数作为写入数据
	strcpy(w_buf,argv[1]);
	nwrite=write(fd,w_buf,sizeof(w_buf));
	printf("write %s %d to the FIFO\n",w_buf,nwrite);
	close(fd);
}
  • 假设我们已经创建了一个名为myfifo的有名管道(在shell下用mknod命令)
  • 如果生成了有名管道,就可以用一般的文件I/O函数,如open、close、read、write等来对它进行操作
  • 写2个程序:一个写管道,一个读管道

读有名管道示例

int main()
{ 
    int fd;
    int count = 1;
    char cache[80];
    fd =open("myfifo",O_RDONLY|O_NONBLOCK);   
    if(fd == -1)
    { 
        perror("open error");  return -1; 
    } 
   while(1){                                                                     
	memset(cache,0, sizeof(cache));
	if((read(fd,cache, 100)) == 0 ){    
	//如果没读到数据                     
		printf("nodata:\n");
	}
	else
		printf("getdata:%s\n", cache);             
	sleep(1); //休眠1s
 }

有名管道使用注意

  • FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
  • 打开时,没有O_RDWR模式,困为管道是单向传输数据的(write总是往末尾添加数据,read则总是从开头返回数据)
  • 打开管道时使用O_NONBLOCK的影响
    • 对一个空的、阻塞的FIFO的read调用将待,直到有数据可以读时才继续执行,与此相反,对一个空的,非阻塞的FIFO进行read调用会立即返回0。
    • 如果open的时候没有指定O_NONBLOCK标志,且open的是写端时,如果不存在此FIFO的已经打开的读端时,open会一直阻塞到有FIFO的读端打开;如果已经存在此FIFO的打开的读端时,open会直接成功返回
    • 打开文件默认是阻塞方式
#include 
#include 
int mkfifo(const char * pathname, mode_t mode)

若第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了
mkfifo(管道名, O_CREAT|O_EXCL) O_EXCL: 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
mode:属性
一旦创建了一个FIFO,就可用open打开它,一般的文件访问函数(close、read、write等)都可用于FIFO。

当打开FIFO时,非阻塞标志(O_NONBLOCK)将对以后的读写产生如下影响:
1、没有使用O_NONBLOCK:访问要求无法满足时进程将阻塞。如试图读取空的FIFO,将导致进程阻塞。
2、使用O_NONBLOCK:访问要求无法满足时不阻塞,立刻出错返回,errno是ENXIO。

SystemV共享内存API

  • shmXXX函数族来实现利用共享内存进行存储的
    • int shmget(key_t key, size_t size, int flag); 获得或得到一个共享存储标识符
    • int shmctl(int shmid, int cmd, struct shmid_ds *buf);对共享存储段执行多种操作
    • void *shmat(int shm_id, const void *shm_addr, int shmflg); 把共享内存连接到当前进程的地址空间
    • shmdt() : detach,分离共享内存

shmget( )

函数原型

int shmget(key_t key,int size,int shmflg);

参数含义:
系统调用shmget()中的第一个参数是关键字值。key标识共享内存的键值: 0/IPC_PRIVATE。 当key的取值为IPC_PRIVATE,则函数shmget()将创建一块新的共享内存;如果key的取值为0,而参数shmflg中又设置IPC_PRIVATE这个标志,则同样会创建一块新的共享内存。

  • size表示待创建的共享内存的大小
  • shmflg:使用IPC_CREAT,则新创建一个共享的内存段,可以和IPC_EXCL一起使用。
  • 返回值:
    • 如果成功,返回共享内存段标识符;
    • 如果失败,则返回-1

shmat( )

  • 函数原型
int shmat(int shmid,char* shmaddr,int shmflg);
  • 参数
    • 参数shmaddr指定共享内存映射到用户空间的地址,如果为0,那么系统则自动找出一个没有映射的内存区域
    • shmid指创建的某块共享内存的引用标识符
    • shmflg为0
  • 返回值:
    • 如果成功,则返回共享内存段连接到进程中的地址
    • 如果失败,则返回-1
  • 内存段正确连接到进程以后,进程中就有了一个指向该内存段的指针。以后就可以使用指针来读取此内存段了。但要注意不能丢失该指针的初值。

shmdt( )

  • 函数原型
   int shmdt(char* shmaddr); 
  • 当一个进程不在需要共享的内存段时,它将会把内存段从其地址空间中脱离。但这不等于将共享内存段从系统内核中移走。

shmctl( )

  • 函数调用形式:
int shmctl(int shmqid,int cmd,struct shmid_ds *buf);
  • cmd参数可以为下列值之一:
    • IPC_STAT读取一个内存段的数据结构shmid_ds,并将它存储在buf参数指向的地址中
    • IPC_SET设置内存段的数据结构shmid_ds中的元素ipc_perm的值。从参数buf中得到要设置的值。
    • IPC_RMID标志内存段为移走。命令IPC_RMID并不真正从系统内核中移走共享的内存段,而是把内存段标记为可移除。

POSIX共享内存

int shm_open(const char *name, int oflag, mode_t mode);
创建或打开一个共享内存,成功返回一个整数的文件描述符,错误返回-11.name:共享内存区的名字;

2.oflag标志位;open的标志一样,基本标志
 O_RDONLY         1    只读打开                        
 O_WRONLY         2    只写打开                        
 O_RDWR           4    读写打开
附加标志:还可选择以下模式与以上3种基本模式组合,如 O_RDWR|O_CREAT:
   O_CREAT     0x0100   创建一个文件并打开   ( 如果指定文件不存在,则创建这个文件)              
   O_TRUNC     0x0200   若存在文件打开,并将文件长度设置为0,其他属性保持         
   O_EXCL      0x0400   如果要创建的文件已存在,则返回 -1,并且修改 errno 的值                           
   O_APPEND    0x0800   追加打开文件                     
   O_TEXT      0x4000   打开文本文件翻译CR-LF控制字符     
   O_BINARY    0x8000   打开二进制字符,不作CR-LF翻译
O_NONBLOCK     如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O 设置为非阻塞模式(nonblocking mode)

3.mode权限位
通常用数字,如#define OPEN_MODE  0777
注意数字前有0
shm_open返回文件描述符
一个整数,标识着一个文件,文件的属性是前面open中标志位所指定的
编译时要加库文件-lrt

POSIX 的共享内存API

  • shm_open():创建共享内存段或连接到现有的已命名内存段。这个系统调用返回一个文件描述符。
  • shm_unlink():根据(shm_open()
    返回的)文件描述符,删除共享内存段。实际上,这个内存段直到访问它的所有进程都退出时才会删除,这与在 UNIX 中删除文件很相似。但是,调用
    shm_unlink() (通常由原来创建共享内存段的进程调用)之后,其他进程就无法访问这个内存段了。
  • mmap():把共享内存段映射到进程的内存。这个系统调用需要
  • shm_open()
    返回的文件描述符,它返回指向内存的指针。(在某些情况下,还可以把一般文件或另一个设备的文件描述符映射到内存。具体方法请查阅操作系统的mmap() 文档。)
  • munmap():作用与 mmap() 相反。
  • msync():用来让共享内存段与文件系统同步 — 当把文件映射到内存时,这种技术有用

内存映射mmap()

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset ) 
  • mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存
  • 参数fd为即将映射到进程空间的文件描述字,一般由open()返回
  • len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起
  • prot 参数指定共享内存的访问权限:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC(可执行), PROT_NONE(不可访问)
  • offset参数一般设为0,表示从文件头开始映射
  • flags通常是:MAP_SHAREDMAP_PRIVATE
  • 参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针NULL,此时选择起始地址的任务留给内核来完成

mmap()

  • mmap()函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址
add_w = mmap(NULL, 1024, PROT_WRITE, MAP_SHARED, fd, 0);
 memcpy(add_w, "helloworld", sizeof("helloworld"));
  • 文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小

mmap例—映射写

typedef struct{
  char name[4];
  int  age;
}people;

main(int argc, char** argv) // map a normal file as shared mem:
{
  int fd,i;
  people *p_map;
  char temp;

  fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
  p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
        MAP_SHARED,fd,0 );
  close( fd );
  temp = 'a';
  for(i=0; i<10; i++)  {
    temp += 1;    memcpy( ( *(p_map+i) ).name, &temp,2 ); ( *(p_map+i) ).age = 20+i;
  }
  printf(" initialize over \n ")sleep(10);
  munmap( p_map, sizeof(people)*10 );  printf( "umap ok \n" );
}

mmap映射例—读

typedef struct{
  char name[4];   int  age;
}people;

main(int argc, char** argv)      // map a normal file as shared mem:
{
  int fd,i;
  people *p_map;
  fd=open( argv[1],O_CREAT|O_RDWR,00777 );
  p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
       MAP_SHARED,fd,0);
  for(i = 0;i<10;i++)
  {
  printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age );
  }
  munmap( p_map,sizeof(people)*10 );
}

共享内存通信

当使用完时,原来的进程调用 munmap()shm_unlink()

进程间通信-共享内存

  • 共享内存方式的缺点:
    • 应用接口和原理很简单,内部机制复杂
    • 共享内存没有提供同步的机制,要借助其他的手段(信号量)来进行进程间的同步工作
    • 通过调用mmap()映射普通文件进行进程间通信时,一定要注意考虑进程何时终止对通信的影响

常见信号含义

SIGHUP:终端上发出的结束信号。
SIGINT:来自键盘的中断信号(CTRL+C),默认操作是终止当前进程的运行。
SIGQUIT:来自键盘的退出信号(CTRL +\),默认操作是将当前进程终止。
SIGFPE:浮点异常信号。
SIGKILL:该信号结束接收信号的进程。
SIGALRM:由用户调用alarm( )函数产生,默认操作是将正在执行的进程杀死掉。
SIGTERM:kill发送出的信号。
SIGCHLD:标识子进程停止或结束的信号。
SIGSTOP:键盘(CTRL+Z)或调试程序的停止信号。

发送信号

kill函数
进程通过kill函数向包括它本身在内的其他进程发送一个信号(除了root用户,kill不能向其他用户拥有的进程发送信号)

int kill(pid_t pid, int sig);

把信号sig发送给进程号为pid的进程

alarm函数

unsigned int alarm(unsigned int seconds);  

seconds秒之后安排发送一个SIGALRM信号给进程

raise函数

int raise(int sig);

向进程本身发送一个sig信号;调用成功返回0;否则返回-1

pause函数

pause 使当前进程暂停,进入睡眠状态。直到当前进程收到“任何信号”,执行相应的响应函数后,pause函数才返回:即继续往下执行

// testPause.c
#include 
#include 
#include 
#include 
#include 
int main()
{
    alarm(1);
    pause();
    printf("I have been waken up\n");
}

程序本意是想暂停1秒后输出
但执行的结果并不是,原因在于
alarm的默认处理是结束进程。
修改程序,使得程序能正常运行
tips:使用signal函数

信号的处理

收到信号后,三种处理方法:
自定义处理函数 如 signal(信号, 函数名);
忽略信号

 signal(SIGINT, SIG_IGN);

SIGKILLSIGSTOP不能忽略
由系统进行默认处理

 signal( SIGINT, SIG_DFL);

嵌入式系统及应用Linux学习笔记(六)---进程通信_第3张图片
信号的处理——signal函数示例

void ouch(int sig)  
{  
    printf("\nOUCH! - I got signal %d\n", sig);  
    //恢复终端中断信号SIGINT的默认行为  
    signal(SIGINT, SIG_DFL);  
}  
  
int main()  
{  
    //改变终端中断信号SIGINT的默认行为,使之执行ouch函数  
    //而不是终止程序的执行  
     signal(SIGINT, ouch);  
    while(1)  
    {  
        printf("Hello World!\n");  
        sleep(1);  
    }  
    return 0;  

第一次按下终止命令(ctrl+c)时,进程并没有被终止,面是输出OUCH! - I got signal 2,因为SIGINT的默认行为被signal函数改变了,当进程接受到信号SIGINT时,它就去调用函数ouch去处理,注意ouch函数把信号SIGINT的处理方式改变成默认的方式,所以当再按一次ctrl+c时,进程就像之前那样被终止了。

结果为:
嵌入式系统及应用Linux学习笔记(六)---进程通信_第4张图片

你可能感兴趣的:(Linux学习)