Linux系统编程@进程通信(一)

时间:2023-12-30 10:58:02

进程间通信概述


需要进程通信的原因:

数据传输

资源共享

通知事件

进程控制

Linux进程间通信(IPC)发展由来

Unix进程间通信

基于System V进程间通信(System V:UNIX系统的一个分支)

POSIX进程间通信(POSIX:可移植操作系统接口,为了提高UNIX环境下应用程序的可移植性。很多其他系统也支持POSIX标准(如:DEC OpenVMS和Windows)。)

现在Linux使用的进程间通信方式包括:

共享文件

管道(pipe)、命名管道(FIFO):只能传输无格式的字节流

信号(signal):能够传输的信号量有限  

消息队列(报文队列):克服了上述的缺点

共享内存

信号量

套接字(socket)

Linux系统编程@进程通信(一)

简单的客户端-服务器或IPC模型

Linux系统编程@进程通信(一)

几种进程间通信方式的优缺
PIPE

  缺点:进程必须有相同的祖先,管道不是持久化的

  

FIFO

  缺点

信号

共享内存

消息队列

信号量

套接字

管道通讯


管道的读写规则:http://www.cnblogs.com/mickole/p/3192461.html

管道:(半双工方式)单向的、先进先出的,把一个进程的输出和另一个进程的输入连接起来。一个进程(写进程)在管道尾部写入数据,另一个进程(读进程)从管道的头部读出数据。包括无名管道有名管道无名管道只用于父进程和子进程间的通信,有名管道可用于运行同一系统中的任意两个进程间的通信。管道也是文件。

注意:无论何时,当不再需要读/写端,关闭它!

查看内核中常数PIPE_BUF规定的管道缓存器的大小

$ find ./ -name "*.h" -exec grep "PIPE_BUF" {} \; -print | more

管道的创建

无名管道创建

int pipe(int filedis[2]);   

int pipe_fd[2];
if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}

当一个管道建立时,他会创建两个文件描述符,filedis[0]用于读管道,filedis[1]用于写管道。

管道的关闭

int close(); //使用close分别关闭两个文件描述符

当管道的一端被关闭后,下面两条规则起作用

1.当读(read)一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束(从技术方面考虑,管道的写端还有进程时,就不会产生文件的结束。可以复制一个管道的描述符,使得有多个进程具有写打开文件描述符。但是,通常一个管道只有一个读进程,一个写进程。下一节介绍FIFO时,我们会看到对于一个单一的FIFO常常有多个写进程)。

2.如果写一个读端已被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write出错返回,errno设置为EPIPE。

在写管道时,常数PIPEBUF规定了内核中管道缓存器的大小。如果对管道进行w r i t e调用,而且要求写的字节数小于等于PIPEBUF,则此操作不会与其他进程对同一管道(或F I F O)的w r i t e操作穿插进行。但是,若有多个进程同时写一个管道(或F I F O),而且某个或某些进程要求写的字节数超过PIPEBUF字节数,则数据可能会与其他写操作的数据相穿插。

管道在不同情况下的特定表现

1.一个进程打开了一个管道来写,然而,还什么都没写,此时,有一个进程进来读。

  此时读进程会等待写进程写入东西后再读。

2.管道是空的,又没有进程打开该管道来写,此时有进程进来读。

  此时读进程不会等待,立即去读空的管道。读取的数据也为空。

3.若调用exec…家族,文件描述符/管道会如何?

  exec函数族,介绍见:http://www.cnblogs.com/kwseeker-bolgs/p/4346047.html

  (1)子进程继承文件描述符和管道的拷贝(同时,包括信号状态,调度参数等)

  (2)文件描述符号可以访问,但是相应的逻辑符号名不可访问

  (3)如何将描述符传递给exec调用的程序
    将这些描述符做为内联(inline)参数传递
    使用标准文件描述符:0,1,2 (STDIN_FILENOSTDOUT_FILENO 和 STDERR_FILENO)
    其他打开的文件,其文件描述符随打开的顺序依次为3,4,5...,OPEN_MAX-1

使用无名管道通信实例

通常,进程会先调用pipe,接着调用fork(这两步为了确保只生成一个管道,且子进程生成后能够继承文件描述符),从而创建从父进程到子进程的IPC管道。此过程中父进程与子进程分别有一对文件描述符。刚开始父进程写入,则关闭父进程的读端;子进程读出,则关闭子进程的写端。注意等待写完再读。sleep(2)

 #include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h> int main()
{
int pipe_fd[];
pid_t pid;
char buf_r[];
char* p_wbuf;
int r_num; memset(buf_r,,sizeof(buf_r)); /*创建管道*/
if(pipe(pipe_fd)<)
{
printf("pipe create error\n");
return -;
} /*创建子进程*/
if((pid=fork())==) //子进程
{
printf("\n");
close(pipe_fd[]); //写关闭
sleep(); /*为什么要睡眠*/
if((r_num=read(pipe_fd[],buf_r,))>)
{
printf( "%d numbers read from the pipe is %s\n",r_num,buf_r);
}
close(pipe_fd[]);
exit();
}
else if(pid>) //父进程
{
close(pipe_fd[]); //读关闭
if(write(pipe_fd[],"Hello",)!=-)
printf("parent write1 Hello!\n");
if(write(pipe_fd[]," Pipe",)!=-)
printf("parent write2 Pipe!\n");
close(pipe_fd[]);
sleep();
waitpid(pid,NULL,); /*等待子进程结束*/
exit();
}
return ;
}

命名管道的创建

命名管道实质上是一个文件

命名管道的特点:

1.持久化

2.有属主和访问权限

3.严格遵循先进先出,不支持 lseek等文件定位操作

~~~~~~~~~~~~~~~~~~~~~

创建FIFO

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char* pathname, mode_t mode);
int mknod(const char* pathname, mode_t mode|S_IFIFO, (dev_t)0);

pathname: FIFO文件名

mode:属性同文件操作中的mode.

  S_IFIFO:表示创建一个命名管道

创建FIFO若成功则返回0,失败返回-1.错误原因存于error中。使用perror和strerror可以打印出详细信息。

#include <stdio.h>
#include <errno.h> perror("mkfifo failed");
printf("error: %s\n", strerror(errno));

  错误代码
      EACCESS 参数pathname所指定的目录路径无可执行的权限
      EEXIST    参数pathname所指定的文件已存在。
      ENAMETOOLONG 参数pathname的路径名称太长。
      ENOENT  参数pathname包含的目录不存在
      ENOSPC  文件系统的剩余空间不足
      ENOTDIR 参数pathname路径中的目录存在但却非真正的目录。
      EROFS     参数pathname指定的文件存在于只读文件系统内。

一旦创建了FIFO,就可以用open打开它,一般的文件访问函数(close/read/write等)都可用于FIFO。

FIFO创建读写的模板

//创建
int ret;
...
ret=mkfifo("/tmp/cmd_pipe", S_IFIFO | 0666);
if(ret == 0){
//创建成功
} else ... {
//创建失败
} //写入
pfp=fopen("/tmp/cmd_pipe", "w+");
...
ret=fprintf(pfp, "Here is a test string!\n");
//读出
pfp=fopen("/tmp/cmd_pipe", "r");
...
ret=fgets(buffer, MAX_LINE, pfp);

  

~~~~~~~~~~~~~~~~~~~~~

打开FIFO时,非阻塞标志O_NONBLOCK对以后的读写产生的影响

1.没有使用O_NONBLOCK:访问要求无法满足时进程将阻塞。如试图读取空的FIFO,将导致进程阻塞,直到有进程对命名管道执行写入,才正常返回;同样,打开FIFO文件来写入的操作会等到其他进程打开FIFO 文件来读取后才正常返回。

2.使用O_NONBLOCK:访问要求无法满足时进程不阻塞,立即出错返回,errno是ENXIO。如打开FIFO读取数据但是没有其他进程打开FIFO来写入;或打开FIFO来写入但是没有其他进程对FIFO进行读取。都会立刻返回ENXIO。

打开FIFO文件和普通文件的区别有2点:

第一个是不能以O_RDWR模式打开FIFO文件进行读写操作。这样做的行为是未定义的。

因为我们通常使用FIFO只是为了单向传递数据,所以没有必要使用这个模式。

如果确实需要在程序之间双向传递数据,最好使用一对FIFO或管道,一个方向使用一个。或者采用先关闭在重新打开FIFO的方法来明确改变数据流的方向。

第二是对标志位的O_NONBLOCK选项的用法。

使用这个选项不仅改变open调用的处理方式,还会改变对这次open调用返回的文件描述符进行的读写请求的处理方式。

O_RDONLY、O_WRONLY和O_NONBLOCK标志共有四种合法的组合方式:

  • flags=O_RDONLY:open将会调用阻塞,除非有另外一个进程以写的方式打开同一个FIFO,否则一直等待。
  • flags=O_WRONLY:open将会调用阻塞,除非有另外一个进程以读的方式打开同一个FIFO,否则一直等待。
  • flags=O_RDONLY|O_NONBLOCK:如果此时没有其他进程以写的方式打开FIFO,此时open也会成功返回,此时FIFO被读打开,而不会返回错误。
  • flags=O_WRONLY|O_NONBLOCK:立即返回,如果此时没有其他进程以读的方式打开,open会失败打开,此时FIFO没有被打开,返回-1。
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h> #define FIFO_NAME "/tmp/my_fifo" int main(int argc,char *argv[]){
int res,i;
int open_mode=0;
if(argc < 2){
fprintf(stderr,"Usage:%s<some combination of O_RDONLY,O_WRONLY,O_NONBLOCK\n", *argv);
     exit(EXIT_FAILURE);
}
argv++;
  if(strncmp(*argv,"O_RDONLY",8)==0)
    open_mode|=O_RDONLY;
  if(strncmp(*argv,"O_WRONLY",8)==0)
    open_mode|=O_WRONLY;
  if(strncmp(*argv,"O_NONBLOCK",10)==0)
    open_mode|=O_NONBLOCK;
  for(i = 1;i < argc;++i){
    argv++;
    if(*argv){
      if(strncmp(*argv,"O_RDONLY",8)==0)
        open_mode|=O_RDONLY;
      if(strncmp(*argv,"O_WRONLY",8)==0)
        open_mode|=O_WRONLY;
      if(strncmp(*argv,"O_NONBLOCK",10)==0)
        open_mode|=O_NONBLOCK;
    }
  }
  if(access(FIFO_NAME,F_OK)==-1){
    res=mkfifo(FIFO_NAME,0777);
    if(res!=0){
      fprintf(stderr,"Could not create fifo %s\n",FIFO_NAME);
      exit(EXIT_FAILURE);
    }
  }
  printf("process %d open FIFO with %d\n",getpid(),open_mode);
  res=open(FIFO_NAME,open_mode);
  printf("process %d result %d\n",getpid(),res);
  sleep(5);
  if(res!=-1)
    close(res);
  printf("process %d finished\n",getpid());
  exit(EXIT_SUCCESS);
}

Linux系统编程@进程通信(一)

open函数调用中的参数标志O_NONBLOCK会影响FIFO的读写操作。

规则如下:

  • 对一个空的阻塞的FIFO的read调用将等待,直到有数据可以读的时候才继续执行/
  • 对一个空的非阻塞的FIFO的read调用立即返回0字节。
  • 对一个完全阻塞的FIFO的write调用将等待,直到数据可以被写入时才开始执行。

系统规定:如果写入的数据长度小于等于PIPE_BUF字节,那么或者写入全部字节,要么一个字节都不写入。

注意这个限制的作用
当只使用一个FIF并允许多个不同的程序向一个FIFO读进程发送请求的时候,为了保证来自不同程序的数据块 不相互交错,即每个操作都原子化,这个限制就很重要了。如果能够保证所有的写请求是发往一个阻塞的FIFO的,并且每个写请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据绝不会交错在一起。通常将每次通过FIFO传递的数据长度限制为PIPE_BUF是一个好办法。

在非阻塞的write调用情况下,如果FIFO 不能接收所有写入的数据,将按照下面的规则进行:

  • (1)请求写入的数据的长度小于PIPE_BUF字节,调用失败,数据不能被写入。
  • (2)请求写入的数据的长度大于PIPE_BUF字节,将写入部分数据,返回实际写入的字节数,返回值也可能是0。

其中。PIPE_BUF是FIFO的长度,它在头文件limits.h中被定义。在linux或其他类UNIX系统中,它的值通常是4096字节。

使用FIFO通信实例

向命名管道写入数据,然后读出之后数据就不再存在管道中了。

fifo_write

 #include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FIFO_SERVER "/tmp/myfifo" main(int argc,char** argv)
{
int fd;
char w_buf[];
int nwrite; /*打开管道*/
fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,); if(argc==)
{
printf("Please send something\n");
exit(-);
} strcpy(w_buf,argv[]); /* 向管道写入数据 */
if((nwrite=write(fd,w_buf,))==-)
{
if(errno==EAGAIN) //EAGIN?
printf("The FIFO has not been read yet.Please try later\n");
}
else
printf("write %s to the FIFO\n",w_buf);
}

fifo_read

 #include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FIFO "/tmp/myfifo" main(int argc,char** argv)
{
char buf_r[];
int fd;
int nread; /* 创建管道 */
if((mkfifo(FIFO,O_CREAT|O_EXCL)<)&&(errno!=EEXIST))
printf("cannot create fifoserver\n"); printf("Preparing for reading bytes...\n"); memset(buf_r,,sizeof(buf_r)); /* 打开管道 */
fd=open(FIFO,O_RDONLY|O_NONBLOCK,);
if(fd==-)
{
perror("open");
exit();
}
while()
{
memset(buf_r,,sizeof(buf_r)); if((nread=read(fd,buf_r,))==-)
{
if(errno==EAGAIN)
printf("no data yet\n");
}
printf("read %s from FIFO\n",buf_r);
sleep();
}
pause(); /*暂停,等待信号*/
unlink(FIFO); //删除文件
}

FIFO实现的进程间的聊天程序

 /*程序说明:
观察10-7和10-8两个程序,可以看出两者的实现基本是一样的,只不过对FIFO文件的读写顺序颠倒了一下,两个程序中只要定义FIFO文件名的宏的值对换一下就可以了。分别在两个终端上运行这两个程序,并在10-7端和10-8端输入数据观察它们的运行结果:
10-7端输入输出如下:
$ ./10-7
Server:hello
Client: world
10-8端输入输出如下:
$ ./10-8
Server: hello Client:world
从运行结果可以看出,通过两个命名管道也可以实现进程间的双向通信。*/
/*
原理图
stdin -----> buf <——————————————— frd
| ^
V |
wfd ---------------> buf <————— stdin
*/
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h> #define FIFO_READ "readfifo"
#define FIFO_WRITE "writefifo"
#define BUF_SIZE 1024 int main(void)
{
int wfd,rfd;
char buf[BUF_SIZE];
int len; umask(); //不屏蔽权限
/*写管道是否存在,不存在就创建*/
if (mkfifo(FIFO_WRITE,S_IFIFO|))
{
printf("Can't create FIFO %s because %s",FIFO_WRITE,strerror(errno));
exit();
}
/*以只写方式打开写管道*/
umask();
wfd = open(FIFO_WRITE,O_WRONLY);
if (wfd == -)
{
printf("open FIFO %s error:%s",FIFO_WRITE,strerror(errno));
exit();
}
/*打开读管道,直到client端创建此管道,打开成功*/
while((rfd = open(FIFO_READ,O_RDONLY)) == -)
{
sleep();
} while()
{
printf("Server:");
/*从标准输入读取BUF_SIZE字节数据到buf,若少于BUF_SIZE,有多少读多少*/
fgets(buf,BUF_SIZE,stdin);
/*缓冲中开始4字节为quit,关闭命名管道退出*/
if (strncmp(buf,"quit",) == )
{
close(wfd);
unlink(FIFO_WRITE);
close(rfd);
exit();
}
/*向写管道写入buf的内容*/
write(wfd,buf,strlen(buf));
/*从读管道中读出数据到buf*/
len = read(rfd,buf,BUF_SIZE);
if (len > )
{
buf[len] = '\0';
printf("Client: %s\n",buf);
}
}
}
 #include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h> #define FIFO_READ "writefifo"
#define FIFO_WRITE "readfifo"
#define BUF_SIZE 1024 int main(void)
{
int wfd,rfd;
char buf[BUF_SIZE];
int len; umask();
if (mkfifo(FIFO_WRITE,S_IFIFO|))
{
printf("Can't create FIFO %s because %s",FIFO_WRITE,strerror(errno));
exit();
} if ((rfd = open(FIFO_READ,O_RDONLY)) == -)
{
sleep();
} wfd = open(FIFO_WRITE,O_WRONLY);
if (wfd == -)
{
printf("Fail to open FIFO %S:%S", FIFO_WRITE,strerror(errno));
exit(-);
} while()
{
len = read(rfd ,buf,BUF_SIZE);
if (len > )
{
buf[len] = '\0';
printf("Server: %s\n",buf);
} printf("Client:");
fgets(buf,BUF_SIZE,stdin);
buf[strlen(buf) -] = '\0';
if (strncmp(buf,"quit",) == )
{
close(wfd);
unlink(FIFO_WRITE);
close(rfd);
exit();
}
write(wfd,buf,strlen(buf));
}
}

信号通讯


信号(全称:软中断信号)是系统响应某些条件而产生的一个事件

信号机制是Unix系统中最为古老的进程间通信机制,很多条件可以产生一个信号:

1.当用户按某些键时。

2.硬件异常产生信号:除数为0、无效的存储访问等等。这些情况通常由硬件检测到,将其通知内核,然后内核产生适当的信号通知进程。

3.进程用kill函数将信号发送给另一个进程。

4.用户可用kill命令将信号发送给其他进程。

信号类型 详见《UNIX环境高级编程》 p199

$ kill -l 列出系统所支持的所有信号 或者 $ man 7 signal

常见的几种信号:(信号的定义:/usr/include/asm/signal.h)

SIGHUP: 从终端上发出的借宿信号

SIGINT: 来自键盘的中断信号

SIGKILL: 该信号结束接收信号的进程

SIGTEMP: kill命令发出的信号

SIGCHLD: 标识子进程停止或结束的信号

SIGSTOP: 来自键盘(Ctrl+Z)或调试程序的停止执行信号

信号的状态

1. 产生

2. 等待:并不是信号一产生,就立即到达目标进程,信号在产生和到达中间的状态就为等待。

  进程可以对到达的信号进行阻止(block),如果被阻止的信号到达进程,该信号的状态就会一直保持等待,直到:
  进程解除对该信号的阻止
  进程忽略该信号  

3. 到达

信号处理

1.忽略此信号,不做任何处理

  SIGKILL和SIGSTOP不能被忽略。

2.执行用户希望的动作

  通知内核在某种信号发生时,调用一个用户函数。在用户函数中,执行用户希望的处理。

3.执行系统默认动作

  对大多数信号的系统默认动作是终止该进程。

信号的发送

主要函数有 kill和raise,kill既可以向自身发送信号,也可以向其他进程发送信号。raise函数是向进程自身发送信号。

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);

kill的pid参数有四种不同的情况
• pid > 0 将信号发送给进程I D为p i d的进程。
• pid == 0 将信号发送给其进程组I D等于发送进程的进程组I D,而且发送进程有许可权向其发送信号的所有进程。
这里用的术语“所有进程”不包括实现定义的系统进程集。对于大多数U N I X系统,系统进程集包括:交换进程(pid 0),init (pid 1)以及页精灵进程(pid 2)。
• pid < 0 将信号发送给其进程组I D等于p i d绝对值,而且发送进程有许可权向其发送信号的所有进程。如上所述一样,“所有进程”并不包括系统进程集中的进程。
• pid == -1 POSIX.1未定义此种情况。

命令行向正在运行哦程序发送信号

$ kill -signo targetPID (如:$ kill -9 4824 )

$ kill targetPID  (向当前进程发送SIGTERM信号,然后进程会终止)

...
raise(SIGUSR1);
...
signal(SIGUSR1, sigusr1_handler);
...

信号的发出、等待以及到达

在Linux系统中,信号的可靠性是指信号是否会丢失,或者说该信号是否支持排队
SIGHUP (1号) ~SIGSYS(31号): 不可靠
* 如果被阻止的信号在解除阻止之前产生了多次,大部分的UNIX系统/Linux都不会将多个同种信号进行队列处理,而是只算一次。
lSIGRTMIN(33号)~SIGRTMAX(64号):可靠信号
* POSIX.4上增加实时扩展功能才能支持多个同种pending信号的队列化处理

alarm函数(设置发送信号的计时器) 

使用alarm函数可以设置一个时间值(闹钟时间),在将来的某个时刻该时间值会被超过。当所设置的时间值被超过后,产生SIGALRM信号。如果不忽略或不捕捉此信号,则其默认动作是终止该进程

#include <unistd.h>
unsigned int alarm(unsigned int seconds); //返回0或以前设置的闹钟时间的余留秒数

两个重要的特性:

*如果有一个之前所安排的信号悬而未决,则此调用会取消该警报,将它替换成刚才所请求的警报所剩下的秒数;

*如果seconds的值为0,则之前的警报(如果有)会被取消,但是不会安排新的警报

pause函数 

pause函数使调用进程挂起直至捕捉到一个信号。

#include <unistd.h>
int pause(void);

只有执行了一个信号处理程序并从其返回时,pause才返回。在这种情况下, pause返回-1,errno设置为EINTR。

调用进程进入可中断的休眠状态

调用schedule() 调度新的进程执行

实例:

signal函数(最简单最旧的信号管理接口)

#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int) //成功则为以前的信号处理配置
                             //失败返回SIG_ERR
//等价于
typedef void (*sighandler_t)(int)
sighandler_t signal(int signo,sighandler_t handler)

signo参数是表10-1中的信号名。func的值是: (a)常数SIG_IGN,(b)常数SIG_DFL,(c)当接到此信号后要调用的函数的地址。

func值的定义

// /usr/include/bits/signum.h
#define SIG_ERR ((__sighandler_t) -1) /*Error return*/
#define SIG_DFL ((__sighandler_t) 0) /*Default action*/
#define SIG_IGN ((__sighandler_t) 1) /*Ignore signal*/

信号表(signo的值):最后一列指明,信号触发后,系统的默认动作。

Linux系统编程@进程通信(一)

Linux系统编程@进程通信(一)

如果指定SIGIGN( 如:signal(SIGINT, SIG_IGN) ),则向内核表示忽略此信号。(记住有两个信号SIGKILL和SIGSTOP不能忽略。)如果指定SIGDFL( 如:signal(SIGINT, SIG_DFL) ),则表示接到此信号后的动作是系统默认动作(见表10-1中的最后1列)。当指定函数地址( 如:signal(SIGINT, func) )时,我们称此为捕捉此信号。我们称此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)。

信号处理模板

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signo,sighandler_t handler) {
if (handler == SIG_ERR){
……
} else if (handler == SIG_DEL){
……
} else if (handler == SIG_IGN){
……
} else {
handler();
}
……
}

使用signal函数避免僵尸进程

在Linux系统编程@进程管理(一)中已经提到避免僵尸进程的这三种方法,现在看下代码实现

 #include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h> void handler(int num){
int status;
int pid=waitpid(-,&status,WNOHANG);
if (WIFEXITED(status)){
printf("The child %d exit with code %d\n", pid, WEXITSTATUS(status));
}
} int main(int argc, char const *argv[])
{
pid_t pid, pid1;
int i; signal(SIGCHLD, handler); if ((pid=fork())){
pid1=pid;
printf("The child process is %d\n", pid1);
for (i = ; i < ; i++)
{
printf("Do parent things\n");
sleep();
}
exit();
} else {
printf("I am a child.\n");
sleep();
exit();
}
return ;
}

sigaction()系统调用

signal方式的缺点

* 无法获知信号被发送的原因

* 处理函数过程中无法阻塞其他信息

sigaction系统调用的函数声明

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
//signo: 需要处理的特定信号
//act: signo信号对应的处理动作
//oldact: 存储signo信号原来对应的处理动作
//成功返回0,失败返回-1

/usr/include/sys/signal.h

struct sigaction{
void(*sa_handler)(int); //老类型的处理函数指针
void(*sa_sigaction)(int,siginfo_t*,void*);//新类型的处理函数指针
sigset_t sa_mask; //将要被阻塞的信号集合
int sa_flags; //信号处理方式掩码
}
//sa_flags状态:
//SA_RESETHAND 处理函数被调用时重置
//SA_NODEFER 在处理信号时,如果又发生了其它的信号,则立即进入其它信号的处理,等其它信号处理完毕后,再继续处理当前的信号,即递归地处理
//SA_RESTART 如果在发生信号时,程序正阻塞在某个系统调用,例如调用read()函数,则在处理完毕信号后,接着从阻塞的系统返回
//SA_SIGINFO sa_sigaction没有设置,使用sa_handler处理函数
sa_sigaction设置,传递给函数信号编号、信号产生原因和结构体

在GNU C库中,当建立信号和handler之间的关系时,默认除了SA_RESTART flag外,其余flags都是默认设为0的。SA_RESTART flag设置为1或者0是由siginterrupt(int sig, int flag)函数决定的。

 

 #include <signal.h>
#include <stdio.h>
#include <stdlib.h> void my_func(int sign_no)
{
if(sign_no==SIGINT)
printf("I have get SIGINT\n");
else if(sign_no==SIGQUIT)
printf("I have get SIGQUIT\n");
}
int main()
{
printf("Waiting for signal SIGINT or SIGQUIT \n");
/*注册信号处理函数*/
signal(SIGINT,my_func);
signal(SIGQUIT, my_func);
pause();
exit();
}

首先让此程序运行$./signal ,然后使用kill为其发送信号 $ kill -s [signo] [pid]。

$ kill [options] signo 目标进程ID

[options]: -s -l -p -a

[目标进程ID]: n 0 -1 -n

  

共享内存


共享内存:被多个进程共享的一部分物理内存。共享内存是进程间共享数据的一种最快的方法,一个进程向共享内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。使用共享存储的唯一窍门是多个进程之间对一给定存储区的同步存取。若服务器将数据放入共享存储区,则在服务器做完这一操作之前,客户机不应当去取这些数据。通常,信号量被用来实现对共享存储存取的同步。记录锁也可用于这种场合。

共享内存的实现步骤:

1.创建共享内存,使用shmget函数;

2.映射共享内存,将这段创建的共享内存映射到具体的进程空间去,使用shmat函数。

共享内存的创建

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h> int shmget(key_t key, int size, int flag) ;
//返回:若成功则为共享内存ID,若出错则为- 1

key: 0/IPC_PRIVATE, 当key的取值为IPC_PRIVATE,shmget()将创建一块新的共享内存;如果key的取值为0,而参数shmflg中又设置IPC_PRIVATE这个标志,同样会创建一块新的共享内存。

共享内存的映射

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid, void *addr, int flag) ;
//返回:若成功则为指向共享存储段的指针,若出错则为- 1

shmid:shmget函数返回的共享存储标志符。

flag:决定以什么方式来确定映射的地址(通常为0)。

共享存储段连接到调用进程的哪个地址上与addr参数以及在flag中是否指定SHMRND位
有关。
(1) 如果addr为0,则此段连接到由内核选择的第一个可用地址上。
(2) 如果addr非0,并且没有指定SHMRND,则此段连接到addr所指定的地址上。
(3) 如果addr非0,并且指定了SHMRND,则此段连接到( addr-(addr mod SHMLBA))
所表示的地址上。SHMRND命令的意思是:取整。SHMLBA的意思是:低边界地址倍数,它总是2的乘方。该算式是将地址向下取最近1个SHMLBA的倍数。
除非只计划在一种硬件上运行应用程序(这在当今是不大可能的),否则不用指定共享段所连接到的地址。所以一般应指定addr为0,以便由内核选择地址。
如果在flag中指定了SHMRDONLY位,则以只读方式连接此段。否则以读写方式连接此段。

共享内存的映射解除

当一个进程不再需要共享内存时,需要把它从进程地址空间中脱离。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr) ;
//返回:若成功则为0,若出错则为- 1

addr 为以前调用shmat时的返回值。

 #include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h> #define PERM S_IRUSR|S_IWUSR
/* 共享内存 */ int main(int argc,char **argv)
{
int shmid;
char *p_addr,*c_addr; if(argc!=)
{
fprintf(stderr,"Usage:%s\n\a",argv[]);
exit();
} /* 创建共享内存 */
if((shmid=shmget(IPC_PRIVATE,,PERM))==-)
{
fprintf(stderr,"Create Share Memory Error:%s\n\a",strerror(errno));
exit();
} /* 创建子进程 */
if(fork()) // 父进程写
{
p_addr=shmat(shmid,,);
memset(p_addr,'\0',);
strncpy(p_addr,argv[],);
wait(NULL); // 释放资源,不关心终止状态
exit();
}
else // 子进程读
{
sleep(); // 暂停1秒
c_addr=shmat(shmid,,);
printf("Client get %p\n",c_addr);
exit();
}
}

消息队列


消息队列就是一个消息的链表。进程可以向其中按照一定的规则添加新消息;另一些数据则可以从消息队列中读走消息。

两种消息队列

POSIX消息队列

系统V消息队列(目前被大量的使用):随内核持续的,只有在内核重启或者人工删除时,该消息队列才会被删除。

消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以要获得一个消息队列的描述字,必须提供该消息队列的键值。

键值获取

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(char *pathname, char proj); //filetokey
//返回文件对应的键值。pathname:文件名;proj:项目名(不为0即可)。

获取消息队列的描述字

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg)
//key:键值,由ftok获得;msgflg:标志位;返回值:与键值key相对应的消息队列描述字

msgflg取值:

IPC_CREAT:创建新的消息队列

IPC_EXCL:与IPC_CREAT一同使用,表示如果要创建的消息队列已经存在,则返回错误。

IPC_NOWAIT:读写消息队列要求无法得到满足时,不阻塞(读时,即使为空也不阻塞)。

创建消息队列的两种情况

1.如果没有与键值key相对应的消息队列,并且msgflg中包含了IPC_CREAT标志位。

2.key参数为IPC_PRIVATE。

int open_queue(key_t keyval)
{
int qid;
if((qid=msgget(keyval,IPC_CREAT))==-1)
{
return -1;
}
return(qid);
}

发送消息

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid,struct msgbuf* msgp, int msgsz, int msgflg); //msgsz (message size)
//向消息队列中发送一条消息
struct msgbuf
{
long mtype; //消息类型>0;
char mtext[n]; //消息数据
}

Questions:

1.消息类型mtype有什么用?

接收消息

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgrcv(int msqid, struct msgbuf* msgp, int msgsz, long msgtyp, int msgflg)
//从msqid代表的消息队列中读取一个msgtyp类型的消息,并把消息存储在msgp指向的msgbuf结构中。在成功地读取了一条消息以后,队列中的这条消息将被删除。

 int read_message(int qid, long type, struct mymsgbuf* qbuf)
{
int result,length;
length=sizeof(struct mymsgbuf)-sizeof(long);
if((result=msgrcv(qid,qbuf,length,type,))==-)
return -;
return(result);
}

msg.c

 #include <sys/types.h>
#include <sys/msg.h>
#include <unistd.h> struct msg_buf
{
int mtype;
char data[];
}; int main()
{
key_t key;
int msgid;
int ret;
struct msg_buf msgbuf; key=ftok("/tmp/2",'a');
printf("key =[%x]\n",key);
msgid=msgget(key,IPC_CREAT|); /*通过文件对应*/ if(msgid==-)
{
printf("create error\n");
return -;
} msgbuf.mtype = getpid();
strcpy(msgbuf.data,"test haha");
ret=msgsnd(msgid,&msgbuf,sizeof(msgbuf.data),IPC_NOWAIT);
if(ret==-)
{
printf("send message err\n");
return -;
} memset(&msgbuf,,sizeof(msgbuf));
ret=msgrcv(msgid,&msgbuf,sizeof(msgbuf.data),getpid(),IPC_NOWAIT);
if(ret==-)
{
printf("recv message err\n");
return -;
}
printf("recv msg =[%s]\n",msgbuf.data); }

消息队列控制

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>    
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

成功返回0,失败返回-1。

参数

说明

msqid

已打开的消息队列id

cmd

控制类型选项

IPC_STAT:取得队列状态

IPC_SET:设置队列属性

IPC_RMID:删除消息队列

buf

存放队列的属性结构

队列属性如下:

struct msqid_ds
{
  struct ipc_perm msg_perm; /* structure describing operation permission */
  __time_t msg_stime; /*最后一次发送消息的时间 */
  unsigned long int __unused1; /*保留*/
  __time_t msg_rtime; /* 最后一次接收数据时间 */
  unsigned long int __unused2;     /*保留*/
  __time_t msg_ctime; /* 最后修改时间 */
  unsigned long int __unused3; /*保留*/
  unsigned long int __msg_cbytes; /* 当前队列字节数 */
  msgqnum_t msg_qnum; /* 当前队列的消息数 */
  msglen_t msg_qbytes; /* 队列中容量 */
  __pid_t msg_lspid; /* 最后发送消息的进程号 */
  __pid_t msg_lrpid; /* 最后接收队列的进程号*/
  unsigned long int __unused4; /*保留*/
  unsigned long int __unused5; /*保留*/
};

信号量(信号灯)


主要用途:保护临界资源。进程可以根据它判断是否能够访问某些共享资源。除了用于访问控制外,还可以用于进程的同步。

比如:A和B进程都想访问同一个临界资源,临界资源未被占用时,信号量提示进程资源未占用,若A先占用了临界资源,则信号量会改变显示资源不可用,这时B过来访问此资源获得信号量(不可用),则会阻塞。等待占用进程释放资源。

信号量分类

1.二值信号灯:信号灯的值只有0和1,类似互斥锁。但信号灯强调共享资源,只要共享资源可用,其他进程同样可以修改信号灯的值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。

2.计数信号灯:信号灯的值可以取任意值。用于可以多个进程同时访问的资源。

创建

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
//key:键值,由ftok获得;nsems:指定打开或者新创建的信号灯集中将包含信号灯的数目;semflg:标识,同消息队列。

信号灯集:信号量的集合

int semop(int semid, struct sembuf* sops,unsigned nsops)
//对信号量进行控制
//semid:信号量集的ID;sops:是一个操作数组,标明要进行什么操作;nsops:sops所指向的数组的元素个数(有多少个操作)。 struct sembuf{
unsigned short sem_num; /*semaphore index in array*/
short sem_op; /*semaphore operation*/
short sem_flg; /*operation flags*/ //IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
                          //IPC_UNDO : 程序结束时(无论正常与否)释放信号量,目的是避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源的永远锁定。
};