2015-10-03 10:42:15 SYP35 阅读数 3215
在UNIX系统中进程由以下三部分组成:①进程控制块PCB;②数据段;③正文段。
        UNIX系统为了节省进程控制块所占的内存空间,把每个进程控制块分成两部分。一部分常驻内存,不管进程是否正占有处理器运行,系统经常会对这部分内容进行查询和处理,常驻部分内容包括:进程状态、优先数、过程特征、数据段始址、等待原因和队列指针等,这是进行处理器调度时必须使用的一些主要信息。另一部分非常驻内存,当进程不占有处理器时,系统不会对这部分内容进行查询和处理,因此这部分内容可以存放在磁盘的对换区中,它随用户的程序和数据部分换进或换出内存。
        UNIX系统把进程的数据段又划分成三部分:用户栈区(供用户程序使用的信息区);用户数据区(包括用户工作数据和非可重入的程序段);系统数据区(包括系统变量和对换信息)。
        正文段是可重入的程序,能被若干进程共享。为了管理可共享的正文段,UNIX设置了一张正文表,每个正文段都占用一个表目,用来指出该正文段在内存和磁盘上的位置、段的大小以及调用该段的进程数等情况。
2013-03-10 19:29:00 DLUTBruceZhang 阅读数 5578

后台进程

默认情况下,进程是在前台运行的,这时就把shell给占据了,我们无法进行其它操作。对于那些没有交互的进程,很多时候,我们希望将其在后台启动,可以在启动参数的时候加一个'&'实现这个目的。

    tianfang > run &
    [1] 11319
    tianfang >

进程切换到后台的时候,我们把它称为job。切换到后台时会输出相关job信息,以前面的输出为[1] 11319例:[1]表示job ID是1,11319表示进程ID是11319。

切换到后台的进程,仍然可以用ps命令查看:

    tianfang > ps
     PID TTY TIME CMD
     5170 pts/3 00:00:00 bash
    11319 pts/3 00:00:00 run
    11320 pts/3 00:00:00 ps
    tianfang >

可以通过jobs命令只查看所有job(后台进程):

    tianfang > jobs
    [1]+ Done run
    tianfang >


守护进程

如果一个进程永远都是以后台方式启动,并且不能受到Shell退出影响而退出,一个正统的做法是将其创建为守护进程。守护进程值得是系统长期运行的后台进程,类似Windows服务。守护进程信息通过ps –a无法查看到,需要用到–x参数,当使用这条命令的时候,往往还附上-j参数以查看作业控制信息,其中TPGID一栏为-1就是守护进程。

    tianfang > ps xj
     PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
     953 1190 1190 1190 ? -1 Ss 1000 0:00 /bin/sh /usr/bin/startkde
     1 1490 1482 1482 ? -1 Sl 1000 0:00 /usr/bin/VBoxClient –seamless
     1 1491 1477 1477 ? -1 Sl 1000 0:00 /usr/bin/VBoxClient –display

创建守护进程最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader。成功调用该函数的结果是:

  • 创建一个新的Session,当前进程成为Session Leader,当前进程的id就是Session的id
  • 创建一个新的进程组,当前进程成为进程组的Leader,当前进程的id就是进程组的id
  • 如果当前进程原本有一个控制终端,则它失去这个控制终端,成为一个没有控制终端的进程。

一个示例如下:

    #include <stdlib.h>
    #include <stdio.h>
    #include <fcntl.h>

    void daemonize(void)
    {
        pid_t pid;

        /*
         * Become a session leader to lose controlling TTY.
         */
        if ((pid = fork()) < 0) {
            perror("fork");
            exit(1);
        } else if (pid != 0) /* parent */
            exit(0);
        setsid();

        /*
         * Change the current working directory to the root.
         */
        if (chdir("/") < 0) {
            perror("chdir");
            exit(1);
        } 

        /*
         * Attach file descriptors 0, 1, and 2 to /dev/null.
         */
        close(0);
        open("/dev/null", O_RDWR);
        dup2(0, 1);
        dup2(0, 2);
    }

    int main(void)
    {
        daemonize();
        while(1)
            sleep(1);
    }

为了确保调用setsid的进程不是进程组的Leader,首先fork出一个子进程,父进程退出,然后子进程调用setsid创建新的Session,成为守护进程。

按照守护进程的惯例,通常将当前工作目录切换到根目录,将文件描述符0、1、2重定向到/dev/null。Linux也提供了一个库函数daemon(3)实现我们的daemonize函数的功能,它带两个参数指示要不要切换工作目录到根目录,以及要不要把文件描述符0、1、2重定向到/dev/null。

    tianfang > run
    tianfang > ps xj | grep run
        1 2665 1868 1868 ? -1 Sl 1000 0:05 kdeinit4: krunner [kdeinit] 
        1 27506 27506 27506 ? -1 Ss 1000 0:00 run
    25662 27508 27507 25662 pts/2 27507 S+ 1000 0:00 grep --color=auto run
    tianfang >

运行这个程序,它变成一个守护进程,不再和当前终端关联。用ps命令看不到,必须运行带x参数的ps命令才能看到。另外还可以看到,用户关闭终端窗口或注销也不会影响守护进程的运行。


守护进程补充:setsid()

守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用 的进 程。 Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。 比如,作业规划进程crond,打印进程lpd等。

守护进程的编程本身并不复杂,复杂的是各种版本的Unix的实现机制不尽相同,造成不同 Unix环境下守护进程的编程规则并不一致。需要注意,照搬某些书上的规则(特别是BSD4.3和低版本的System V)到Linux会出现错误的。下面将给出Linux下守护进程的编程要点和详细实例。

一. 守护进程及其特性

守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程序TSR与之相似。其次,守护进程必须与其运行前的环境隔离开来。这些环 境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下 来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还 可以由用户终端(通常是 shell)执行。

总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别。因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。如果对进程有比较深入的认识就更容易理解和编程了。

二. 守护进程的编程要点

前面讲过,不同Unix环境下守护进程的编程规则并不一致。所幸的是守护进程的编程原则其实都一样,区别在于具体的实现细节不同。这个原则 就是要满足守护进程的特性。同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来与BSD4相比更方便。编程要点如下;

1. 在后台运行。

为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。

if(pid=fork())exit(0); //是父进程,结束父进程,子进程继续
2. 脱离控制终端,登录会话和进程组

有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号 (PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。 控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使 进程成为会话组长:

setsid();

说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

3. 禁止进程重新打开控制终端

现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:

if(pid=fork()) exit(0); //结束第一子进程,第二子进程继续(第二子进程不再是会话组长)

4. 关闭打开的文件描述符

进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:

for(i=0;i 关闭打开的文件描述符close(i);>

5. 改变当前工作目录

进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 /tmpchdir("/")

6. 重设文件创建掩模

进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);

7. 处理SIGCHLD信号

处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结 束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下 可以简单地将 SIGCHLD信号的操作设为SIG_IGN。

signal(SIGCHLD,SIG_IGN);

这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。

三. 守护进程实例

守护进程实例包括两部分:主程序test.c和初始化程序init.c。主程序每隔一分钟向/tmp目录中的日志test.log报告运行状态。初始化程序中的init_daemon函数负责生成守护进程。读者可以利用init_daemon函数生成自己的守护进程。

1. init.c清单

#include < unistd.h >

#include < signal.h >

#include < sys/param.h >

#include < sys/types.h >

#include < sys/stat.h >

void init_daemon(void)

{

 int pid;

 int i;

 if(pid=fork())

  exit(0);//是父进程,结束父进程

 else if(pid< 0)

  exit(1);//fork失败,退出

 //是第一子进程,后台继续执行

 setsid();

 //第一子进程成为新的会话组长和进程组长

 //并与控制终 端分离

 if(pid=fork())

  exit(0);//是第一子进程,结束第一子进程

 else if(pid< 0)

  exit(1);//fork失败,退出

 //是第二子进程,继续

 //第二子进程不再是会话组长

 for(i=0;i< NOFILE;++i)

  //关闭打开的文件描述符

  close(i);

  chdir("/tmp"); //改变工作目录到/tmp

  umask(0);//重设 文件创建掩模

  return;

 }
2. test.c清单

#include < stdio.h >

#include < time.h >

void init_daemon(void);//守护进程初始化函数

main()

{

 FILE *fp;time_t t;

 init_daemon();//初始化为Daemon

 while(1)//每隔一分钟向test.log报告运行状态

 {

   sleep(60);//睡 眠一分钟

   if((fp=fopen("test.log","a")) >=0)

   {t=time(0);

    fprintf(fp,"Im here at %sn",asctime(localtime(&t)) );

    fclose(fp);}

   }

}
以上程序在RedHat Linux6.0下编译通过。步骤如下:

编译:gcc -g -o test init.c test.c

执行:./test

查看进程:ps -ef

从输出可以发现test守护进程的各种特性满足上面的要求。   



2016-08-06 00:52:14 caoyan_12727 阅读数 2377
  本文将《unix环境高级编程》一书中所涉及的几种重要的进程间通信方式(Inter-Process Communication)进行简单总结,总的来说,进程间通信有以下几种:
        (1)管道(pipe,未命名管道):适用于两个相关进程间的使用,而且这两个相关的进程还要有一个共同的创建了它们的祖先进程。首先我们先列管道的相关函数。创建一个管道:int pipe(int fd[2]);在历史上,管道是半双工的,数据只能在一个方向上流动。通常,一个管道由一个进程创建,在进程fork之后,这个管道就能在父进程和子进程间使用了。一般的描绘半双工管道的方法如下:



一般说来,fd[0]为读而打开,fd[1]为写而打开,fd[1]的输出是fd[0]的写入.一旦创建了一个管道,我们就可以像读写文件描述符一样
让我们来看一个简单的代码:
#include "apue.h"
int main(void){
	int		n;
	int		fd[2];
	pid_t	pid;
	char	line[MAXLINE]

	if (pipe(fd) < 0)
		err_sys("pipe error");
	if ((pid = fork()) < 0) {
		err_sys("fork error");
	} else if (pid > 0) {		/* parent */
		close(fd[0]);//父进程关闭读入端,则说明父进程使用写入端
		write(fd[1], "hello world\n", 12);//父进程将信息写入管道
	} else {					/* child */
		close(fd[1]);//子进程关闭写入端,使用读端
		n = read(fd[0], line, MAXLINE);//从fd[0]将数据读入到缓冲line中
		write(STDOUT_FILENO, line, n);//将line中的字符写入到标准输出
	}
	exit(0);
}
那么上述代码所形成的管道数据传输示意图就像图15-4所示;
 (2)命名管道(FIFO):
命名管道相关的函数为:

通过FIFO,不相关的进程也能进行通信,FIFO有以下两种用途:
1.shell命令使用FIFO将数据从一条管道复制到另一条管道时无需创建中间临时文件;考虑这样的一个过程,它需要对一个经过过滤的输入流进行两次处理。下图现显示了这种安排。

数据经prog1处理之后要作为prog3和prog2的输入,如果使用管道(pipe)的话,主程序要fork两次,况且一条管道只能维持一对进程的通信,因为管道是半双工,数据只能从一个方向流向另一个方向,主程序prog1不可能通过一个文件描述符向两个子进程传送数据(一个fd[1]只能向一个fd[0]传送数据),所以此时只能将prog1的输出保存到文件,然后再从文件到prog2,这样的话势必会产生磁盘中间文件。但是如果使用FIFO情况就会不一样,我们来看看书中的例子:
mkfifo  fifo1
prog3<fifo1 &
prog<infile|tee fifo1 |prog2
用FIFO就可实现这样的过程而中间文件的产生,如图:

为了了解这个过程我们来看看上面的三条语句使怎样工作的:
   tee命令:在执行Linux命令时,我们可以把输出重定向到文件中,比如 ls >a.txt,这时我们就不能看到输出了,如果我们既想把输出保存到文件中,又想在屏幕上看到输出内容,就可以使用tee命令了。tee命令读取标准输入,把这些内容同时输出到标准输出和(多个)文件中(read from standard input and write to standard output and files. Copy standard input to each FILE, and also to standard output. If a FILE is -, copy again to standard output.)。在info tee中说道:tee命令可以重定向标准输出到多个文件(`tee': Redirect output to multiple files. The `tee' command copies standard input to standard output and also to any files given as arguments.  This is useful when you want not only to send some data down a pipe, but also to save a copy.)。要注意的是:在使用管道线时,前一个命令的标准错误输出不会被tee读取。简言之tee的作用是:输出到标准输出的同时,保存到文件file中。如果文件不存在,则创建;如果已经存在,则覆盖之。
   新建一个命名管道:FIFO,然后后台运行prog3<fifo1 &,这个表示一旦有数据写入到管道fifo1时,prog3就从管道读取数据;prog<infile|tee fifo1 |prog2,这条命令是从infile读入进程prog1所需的输入数据,然后通过prog1对输入数据进行处理,处理之后的输出数据经过|变成tee命令的输入,tee将起输入不仅显示到标准输出,而且还被输出到fifo1文件(unix一切皆可看做是文件);那么fifo1里的数据会被读入到进程prog1,被显示到标准输出的那一份数据又被|命令处理为标准输入被穿到porg2.所以看到整个过程没有中间磁盘文件的产生,虽然要产生fifo1,但是管道文件在磁盘中显示的占用空间大小为0(即不占用磁盘空间)。
2.客户进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程二者之间进行数据传递。
首先用我自己写的小程序来验证FIFO命名管道的基本用法:
/*fifo-write.c*/
#include <stdio.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include<apue.h>
#include <unistd.h>

int main(){
	int fd;
	int nRead;
	char szBuff[100];
	sleep(1);//wait for the creating of fifo3
	fd=open("/home/caoyan/unix/c15/cyfifo/fifo3", O_WRONLY);
  	while(1){
      		if((nRead = read(STDIN_FILENO, szBuff, sizeof(szBuff))) == -1){
          	if (errno == EAGAIN)
            		printf("no data\n");
        	}
      		szBuff[nRead] = '\0';
     	 	write(fd,szBuff,nRead);
		if (szBuff[0] == '#')break;//the last letter is 'Q' means that the data transport is over!
    	}
	printf("data sending has finished!\n");
}
/*fifo-read.c*/
#include <stdio.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include<apue.h>
#include<iostream>
using namespace std;
int main(int argc, char* argv[]){
  int tmp,nRead,fd;
  char szBuff[128];
  const char *szPath="/home/caoyan/unix/c15/cyfifo/fifo3";         
  tmp=mkfifo(szPath,0777);
  if (-1 == fd){
      printf("create fifo error\n");
      return 0;
    }
  fd=open("/home/caoyan/unix/c15/cyfifo/fifo3", O_RDONLY);
  if(fd==-1)exit(1);
  while(1){
      if((nRead = read(fd, szBuff, sizeof(szBuff))) == -1){
          if (errno == EAGAIN)
            printf("no data\n");
        }
      if (szBuff[0] == '#')break;//the last letter is 'Q' means that the data transport is over!
      szBuff[nRead] = '\0';
      printf("%s", szBuff);
    }
   printf("data recieving has finished!\n");
  return 0;
}
测试结果:
数据发送方:

数据接收方:

可以看到两个无关的进程通信成功!!!
让我们来看看通信结束之后的命名管道fifo3:

可以看到管道使用前后大小均为0!!!!
下面我们来看看用FIFO实现客户进程和服务器进程通信的设计方式:

图15-22表示多个客户进程向服务器进程请求数据,每个客户进程可以将其请求写入到一个总所周知的FIFO,如果所有客户进程与服务器进程共用一个FIFO的话(这里的FIFO是另外的一个命名管道,不同于上图中的众所周知的FIFO,用来从服务器进程写入,从客户进程读出数据),服务器进程就不知道如何将数据回送给客户进程,因为一旦有数据被写入到FIFO中,所有的客户进程都可以读里面的数据,这样就会出乱!!!一个好的解决方案如下:
为每个客户进程建立一个FIFO,这样每个客户进程就可以互不干扰地从服务器进程读取数据!!!!

(3)消息队列:
 第一步:创建一个消息队列:int msgget(key_t key,int msgflg),其中key_t ftok( const char * fname, int id ),fname就时你指定的文件名(该文件必须是存在而且可以访问的),id是子序号,虽然为int,但是只有8个比特被使用(0-255)。当成功执行的时候,一个key_t值将会被返回,否则 -1 被返回。在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。如指定文件的索引节点号为65538,换算成16进制为 0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。也是说消息队列号由文件的索引节点号和用户指定ID组成;
第二步:发送消息 :int msgsnd ( int msqid, struct msgbuf *msgp, int msgsz, int msgflg ); 其中的msgbuf的数据结构可表示为:
(1)消息缓冲区(msgbuf)
我们在这里要介绍的第一个数据结构是msgbuf结构,可以把这个特殊的数据结构看成一个存放消息数据的模板,它在include/linux/msg.h中声明,描述如下:
/* msgsnd 和msgrcv 系统调用使用的消息缓冲区*/
struct msgbuf {
    long mtype;         /* 消息的类型,必须为正数 */
    char mtext[1];      /* 消息正文 */
};
注意:消息正文的长度是可以改变的,它的长度可以是1字节也可以是512字节或者更长,这也就是为什么在发送消息的时候要指明消息正文的长度,一般而言,消息正文的前面的消息类型大小是固定的。对于消息数据元素(mtext),不要受其描述的限制。实际上,这个域(mtext)不仅能保存字符数组,而且能保存任何形式的任何数据。这个域本身是任意的,因为这个结构本身可以由应用程序员重新定义:
struct my_msgbuf {
      long    mtype;          /* 消息类型 */
        long    request_id;     /* 请求识别号 */
        struct client info;    /* 客户消息结构 */
};
我们看到,消息的类型还是和前面一样,但是结构的剩余部分由两个其它的元素代替,而且有一个是结构。这就是消息队列的优美之处,内核根本不管传送的是什么样的数据,任何信息都可以传送。
但是,消息的长度还是有限制的,在Linux中,给定消息的最大长度在include/linux/msg.h中定义如下:
#define MSGMAX 8192    /* max size of message (bytes) */
消息总的长度不能超过8192字节,包括mtype域,它是4字节长。
(2)消息结构(msg)
内核把每一条消息存储在以msg结构为框架的队列中,它在include/ linux/msg.h中定义如下:
struct msg {
    struct msg *msg_next;   /* 队列上的下一条消息 */
    long msg_type;          /*消息类型*/
    char *msg_spot;         /* 消息正文的地址 */
    short msg_ts;           /* 消息正文的大小 */
};
注意:msg_next是指向下一条消息的指针,它们在内核地址空间形成一个单链表。
(3)消息队列结构(msgid_ds)
当在系统中创建每一个消息队列时,内核创建、存储及维护这个结构的一个实例。
/* 在系统中的每一个消息队列对应一个msqid_ds 结构 */
struct msqid_ds {
    struct ipc_perm msg_perm;
    struct msg *msg_first;    /* 队列上第一条消息,即链表头*/
    struct msg *msg_last;    /* 队列中的最后一条消息,即链表尾 */
    time_t msg_stime;        /* 发送给队列的最后一条消息的时间 */
    time_t msg_rtime;            /* 从消息队列接收到的最后一条消息的时间 */
    time_t msg_ctime;             /* 最后修改队列的时间*/
    ushort msg_cbytes;          /*队列上所有消息总的字节数 */
    ushort msg_qnum;          /*在当前队列上消息的个数 */
    ushort msg_qbytes;        /* 队列最大的字节数 */
    ushort msg_lspid;           /* 发送最后一条消息的进程的pid */
    ushort msg_lrpid;           /* 接收最后一条消息的进程的pid */
};
那么消息队列的逻辑结构可以表示为:

第四步:接受消息:int msgrcv ( int msqid, struct msgbuf *msgp, int msgsz, long mtype, int msgflg );
返回值:成功,则为拷贝到消息缓冲区的字节数,失败为-1。
很明显,第一个参数用来指定要检索的队列(必须由msgget()调用返回),第二个参数(msgp)是存放检索到消息的缓冲区的地址,第三个参数(msgsz)是消息缓冲区的大小,不包括消息类型mtype的长度。第四个参数(mtype)指定了消息的类型。内核将搜索队列中相匹配类型的最早的消息,并且返回这个消息的一个拷贝,返回的消息放在由msgp参数指向的地址。这里存在一个特殊的情况,如果传递给mytype参数的值为0,就可以不管类型,只返回队列中最早的消息。如果传递给参数msgflg的值为IPC_NOWAIT,并且没有可取的消息,那么给调用进程返回ENOMSG错误消息,否则,调用进程阻塞,直到一条消息到达队列并且满足msgrcv()的参数。如果一个客户正在等待消息,而队列被删除,则返回EIDRM。如果当进程正在阻塞,并且等待一条消息到达但捕获到了一个信号,则返回EINTR。
(4)信号量:
当我们在多用户系统,多进程系统,或是两者混合的系统中使用线程操作编写程序时,我们经常会发现我们有段临界代码,在此处我们需要保证一个进程(或是一个线程的执行)需要排他的访问一个资源。信号量有一个复杂的编程接口。幸运的是,我们可以很容易的为自己提供一个对于大多数的信号量编程问题足够高效的简化接口。为了阻止多个程序同时访问一个共享资源所引起的问题,我们需要一种方法生成并且使用一个标记从而保证在临界区部分一次只有一个线程执行。线程相关的方法,我们可以使用互斥或信号量来控制一个多线程程序对于临界区的访问。信号量与已经介绍过的IPC机构(管道,FIFO以及消息队列不同),它是一个计数器,用于多个为多个进程提供对共享数据的访问。当我们要使用XSI信号量时,首先需要通过调用函数semget来获得一个信号量的ID,函数原型如下
int semget(key_t key,int nsems,int flag);
其中,nsems是该集合中的信号量数,如果是创建新集合(一般是在服务器进程中),则必须指定nsems,如果是应用现有集合(一个客户进程),则将nsems指定为0。
信号量相关的三个重要函数:

1.   semget函数原型

semget(得到一个信号量集标识符或创建一个信号量集对象)
所需头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数说明 得到一个信号量集标识符或创建一个信号量集对象并返回信号量集标识符
函数原型 int semget(key_t key, int nsems, int semflg)
函数传入值 key 0(IPC_PRIVATE):会建立新信号量集对象
大于0的32位整数:视参数semflg来确定操作,通常要求此值来源于ftok返回的IPC键值
nsems 创建信号量集中信号量的个数,该参数只在创建信号量集时有效
msgflg 0:取信号量集标识符,若不存在则函数会报错
IPC_CREAT:当semflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的信号量集,则新建一个信号量集;如果存在这样的信号量集,返回此信号量集的标识符
IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的信号量集,则新建一个消息队列;如果存在这样的信号量集则报错
函数返回值 成功:返回信号量集的标识符
出错:-1,错误原因存于error中
附加说明 上述semflg参数为模式标志参数,使用时需要与IPC对象存取权限(如0600)进行|运算来确定信号量集的存取权限
错误代码 EACCESS:没有权限
EEXIST:信号量集已经存在,无法创建
EIDRM:信号量集已经删除
ENOENT:信号量集不存在,同时semflg没有设置IPC_CREAT标志
ENOMEM:没有足够的内存创建新的信号量集
ENOSPC:超出限制

2.   semop函数原型

semop(完成对信号量的P操作或V操作)
所需头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数说明 对信号量集标识符为semid中的一个或多个信号量进行P操作或V操作
函数原型 int semop(int semid, struct sembuf *sops, unsigned nsops)
函数传入值 semid:信号量集标识符
sops:指向进行操作的信号量集结构体数组的首地址,此结构的具体说明如下:
struct sembuf {
    short semnum; /*信号量集合中的信号量编号,0代表第1个信号量*/
    short val;/*若val>0进行V操作信号量值加val,表示进程释放控制的资源 */
/*若val<0进行P操作信号量值减val,若(semval-val)<0(semval为该信号量值),则调用进程阻塞,直到资源可用;若设置IPC_NOWAIT不会睡眠,进程直接返回EAGAIN错误*/
  /*若val==0时阻塞等待信号量为0,调用进程进入睡眠状态,直到信号值为0;若设置IPC_NOWAIT,进程不会睡眠,直接返回EAGAIN错误*/
    short flag;  /*0 设置信号量的默认操作*/
/*IPC_NOWAIT设置信号量操作不等待*/
/*SEM_UNDO 选项会让内核记录一个与调用进程相关的UNDO记录,如果该进程崩溃,则根据这个进程的UNDO记录自动恢复相应信号量的计数值*/
  };
nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作
函数返回值 成功:返回信号量集的标识符
出错:-1,错误原因存于error中
错误代码 E2BIG:一次对信号量个数的操作超过了系统限制
EACCESS:权限不够
EAGAIN:使用了IPC_NOWAIT,但操作不能继续进行
EFAULT:sops指向的地址无效
EIDRM:信号量集已经删除
EINTR:当睡眠时接收到其他信号
EINVAL:信号量集不存在,或者semid无效
ENOMEM:使用了SEM_UNDO,但无足够的内存创建所需的数据结构
ERANGE:信号量值超出范围

3.   semctl函数原型

semctl (得到一个信号量集标识符或创建一个信号量集对象)
所需头文件 #include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数说明 得到一个信号量集标识符或创建一个信号量集对象并返回信号量集标识符
函数原型 int semctl(int semid, int semnum, int cmd, union semun arg)
函数传入值 semid 信号量集标识符
semnum 信号量集数组上的下标,表示某一个信号量
cmd 见下文表15-4
arg union semun {
   short val;          /*SETVAL用的值*/
   struct semid_ds* buf; /*IPC_STAT、IPC_SET用的semid_ds结构*/
   unsigned short* array; /*SETALL、GETALL用的数组值*/
   struct seminfo *buf;   /*为控制IPC_INFO提供的缓存*/
  } arg;
函数返回值 成功:大于或等于0,具体说明请参照表15-4
出错:-1,错误原因存于error中
附加说明 semid_ds结构见上文信号量集内核结构定义
错误代码 EACCESS:权限不够
EFAULT:arg指向的地址无效
EIDRM:信号量集已经删除
EINVAL:信号量集不存在,或者semid无效
EPERM:进程有效用户没有cmd的权限
ERANGE:信号量值超出范围 

表15-4 semctl函数cmd形参说明表

命令 解   释
IPC_STAT 从信号量集上检索semid_ds结构,并存到semun联合体参数的成员buf的地址中
IPC_SET 设置一个信号量集合的semid_ds结构中ipc_perm域的值,并从semun的buf中取出值
IPC_RMID 从内核中删除信号量集合
GETALL 从信号量集合中获得所有信号量的值,并把其整数值存到semun联合体成员的一个指针数组中
GETNCNT 返回当前等待资源的进程个数
GETPID 返回最后一个执行系统调用semop()进程的PID
GETVAL 返回信号量集合内单个信号量的值
GETZCNT 返回当前等待100%资源利用的进程个数
SETALL 与GETALL正好相反
SETVAL 用联合体中val成员的值设置信号量集合中单个信号量的值
相关的数据结构为:
对于系统中的每个信号量集,内核维护一个如下的信息结构:
struct semid_ds {
    struct ipc_permsem_perm ;
    structsem*    sem_base ; //信号数组指针
    ushort        sem_nsem ; //此集中信号个数
    time_t        sem_otime ; //最后一次semop时间
    time_t        sem_ctime ; //最后一次创建时间
} ;
某个给定信号量的结构体
struct sem {
    ushort_t  semval ;  //信号量的值
    short     sempid ;  //最后一个调用semop的进程ID
    ushort    semncnt ; //等待该信号量值大于当前值的进程数(一有进程释放资源 就被唤醒)
    ushort    semzcnt ; //等待该信号量值等于0的进程数
} ; 
struct sembuf {
    unsigned short sem_num ; //信号量在信号量集中的index(对哪个信号量操作),如果只有一个信号量,则对应的值为0
    short          sem_op ;  //操作的类型(P操作 还是 V操作)
    short          sem_flg ; //是否等待(当信号量的值不够消耗时 是否等待其他进进程释放资源)
} ;
union semun {
   short val;          /*SETVAL用的值*/
   struct semid_ds* buf; /*IPC_STAT、IPC_SET用的semid_ds结构*/
   unsigned short* array; /*SETALL、GETALL用的数组值*/
   struct seminfo *buf;   /*为控制IPC_INFO提供的缓存*/
  }arg;
对于sembuf结构体中的sem_op值:
⑴若sem_op为正,这对应于进程释放占用的资源数。sem_op值加到信号量的值上。(V操作)
⑵若sem_op为负,这表示要获取该信号量控制的资源数。信号量值减去sem_op的绝对值。(P操作)
⑶若sem_op为0,这表示调用进程希望等待到该信号量值变成0
如果此时执行的是p操作且信号量值小于sem_op的绝对值(资源不能满足要求),则:
⑴若指定了IPC_NOWAIT,则semop()出错返回EAGAIN。
⑵若未指定IPC_NOWAIT,则信号量的semncnt值加1(因为调用进程将进入休眠状态),然后调用进程被挂起直至:①此信号量变成大于或等于sem_op的绝对值;②从系统中删除了此信号量,返回EIDRM;③进程捕捉到一个信号,并从信号处理程序返回,返回EINTR。(与消息队列的阻塞处理方式 很相似)
下面我们通过引用http://blog.csdn.net/liang890319/article/details/8280860所提及的例子来简单讲解上面的参数的运用:
/*sem_com.h*/
#ifndef     SEM_COM_H  
#define     SEM_COM_H  
  
#include <sys/ipc.h>  
#include <sys/sem.h>  
  
union semun  {  
    int val;  
    struct semid_ds *buf;  
    unsigned short *array;
    struct seminfo *buf;  
};  
  
int init_sem(int, int);  
int del_sem(int);  
int sem_p(int);  
int sem_v(int);   
  
#endif /* SEM_COM_H */

/* sem_com.c */  
  
#include "sem_com.h"  
int init_sem(int sem_id, int init_value)  {  
    union semun sem_union;  //可以知道对信号量ID的操作都需要用到semun联合体
    //我们想对信号量ID采取什么样的操作就将对应的值设置,然后再设置标志(SETVAL)
    //不同的标志(也就是cmd参数)对应着设置semum联合体里的不同字段的值
    sem_union.val = init_value;  
    if (semctl(sem_id, 0, SETVAL, sem_union) == -1){//设置单个信号量的值
        perror("Initialize semaphore");       
        return -1;  
    }  
    return 0;  
}  
  
int del_sem(int sem_id){//从内核中删除该信号量  
    union semun sem_union;  
    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1){//在semctl中,参数包括对直接操作的信号,但是在semop中,
        perror("Delete semaphore"); //被操作的信号量的下标由sembuf中的相关字段标明 
        return -1;   
    }  
} 

int sem_p(int sem_id)  { //对信号量的操作,操作的信息以sembuf结构体进行传递 
    struct sembuf sem_b;  
    sem_b.sem_num = 0; /*id,因为集合中只有一个信号量,所以下标为0*/  
    sem_b.sem_op = -1; /* P operation,对信号量减1*/  
    sem_b.sem_flg = SEM_UNDO; //这会使得操作系统跟踪当前进程对信号量所做的改变,而且如果进程终止而没
    //有释放这个信号量, 如果信号量为这个进程所占有,这个标记可以使得操作系统自动释放这个信号量。 
    if (semop(sem_id, &sem_b, 1) == -1){//对信号量进行减1操作
        perror("P operation");  
        return -1;  
    }  
    return 0;  
}   
 
int sem_v(int sem_id) { //和sem_p操作相对,对信号量所代表的资源进行释放 
    struct sembuf sem_b;    
    sem_b.sem_num = 0; /* id */  
    sem_b.sem_op = 1; /* V operation */   
    sem_b.sem_flg = SEM_UNDO;   
    if (semop(sem_id, &sem_b, 1) == -1)  {  
        perror("V operation");  
        return -1;  
    }  
    return 0;  
}  

/* fork.c */   
#include <sys/types.h>  
#include <unistd.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/shm.h>  
#define DELAY_TIME      3  
  
int main(void)  {  
    pid_t result;  
    int sem_id;  
    sem_id = semget(ftok(".", 'a'),  1, 0666|IPC_CREAT); /* 创建一个信号量集,这个信号量集中只有一个信号量*/  
    init_sem(sem_id, 0);  //初始值设为0资源被占用   
    result = fork();  /*调用fork函数,其返回值为result*/
    /*通过result的值来判断fork函数的返回情况,首先进行出错处理*/  
    if(result ==  -1)perror("Fork\n");  
    else if (result == 0) {/*返回值为0代表子进程*/  
        printf("Child process will wait for some seconds...\n");  
        sleep(DELAY_TIME);  
        printf("The returned value is %d in the child process(PID = %d)\n", result, getpid());  
        sem_v(sem_id);   //释放资源  
    }  
    else {/*返回值大于0代表父进程*/  
        sem_p(sem_id);     //等待资源,如果子进程不释放 就一直等  
        printf("The returned value is %d in the father process(PID = %d)\n", result, getpid());  
        sem_v(sem_id);     //释放资源  
        del_sem(sem_id);  //删除信号量  
    }  
    exit(0);  
}  
资源的量一开始被设置为0,即代表当前没用空闲的可用资源,在fork()之后不知道是父进程还是子进程先执行,如果是子进程先执行,那么直接释放资源,父进程直接获得资源无需等待;但如果是父进程先执行,就对资源进行p(申请资源)操作,此时假设未指定IPC_NOWAIT字段,则等待该信号量值大于当前值(0)的进程数semncnt的值会加1,然后父进程被阻塞(一有进程释放资源 就被唤醒)。等到子进程释放之后父进程就可以继续执行。在这个例子中由于子进程sleep(),所以子进程阻塞,等到子进程被唤醒,资源被释放,父进程得以继续执行。
    
        
2018-07-26 19:36:04 Dachao0707 阅读数 219

进程间通信(IPC)介绍

进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

一、一些基本概念

进程间通信(IPC):进程之间交换数据的过程叫进程间通信。
    进程间通信的方式:
        简单的进程间通信:
            命令行:父进程通过exec函数创建子进程时可以附加一些数据。
            环境变量:父进程通过exec函数创建子进程顺便传递一张环境变量表。
            信号:父子进程之间可以根据进程号相互发送信号,进程简单通信。
            文件:一个进程向文件中写入数据,另一个进程从文件中读取出来。
            命令行、环境变量只能单身传递,信号太过于简单,文件通信不能实时。
        
        XSI通信方式:X/open 计算机制造商组织。
            共享内存、消息队列、信号量
        网络进程间通信方式:网络通信就是不同机器的进程间通信方式。
        传统的进程间通信方式:管道

二、管道
    1、管道是一种古老的通信的方式(基本上不再使用)
    2、早期的管道是一种半双工,现在大多数是全双工。
    3、有名管道(这种管道是以文件方式存在的)。
    int mkfifo(const char *pathname, mode_t mode);

例子:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>

int main()
{
	// 创建管道文件
	if(0 > mkfifo("test.txt",0644))
	{
		perror("mkfifo");
		return -1;
	}

	// 打开
	int fd = open("test.txt",O_RDWR);
	if(0 > fd)
	{
		perror("open");
		return -1;
	}

	// 准备缓冲区
	char buf[255] = {};
	// 写/读
	while(1)
	{
		printf(">");
		gets(buf);
		int ret = write(fd,buf,strlen(buf));
		printf("写入数据%d字节\n",ret);
		if('q' == buf[0])break;
		getchar();
		bzero(buf,sizeof(buf));
		ret = read(fd,buf,sizeof(buf));
		printf("读取数据%d字节,内容:%s\n",ret,buf);
		if('q' == buf[0])break;
	}
	// 关闭
	close(fd);
}


            
    管道通信的编程模式:
        进程A                进程B
        创建管道mkfifo
        打开管道open            打开管道
        写/读数据read/write    读/写数据
        关闭管道close            关闭管道
            
    4、无名管道:由内核帮助创建,只返回管道的文件描述符,看不到管道文件,但这种管道只能用在fork创建的父子进程之间。
        int pipe(int pipefd[2]);
        pipefd[0] 用来读数据
        pipefd[1] 用来写数据

以Linux中的C语言编程为例。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<strings.h>
#include<sys/types.h>
#include<signal.h>
int main()
{
	int fd[2];
	int pid;
	char buf[255]={};

	if(pipe(fd)<0)
	{
		perror("pipe");
		return -1;
	}
	if((pid=fork())<0)
	{
		perror("fork");
	}
	else if(pid>0)
	{
		
		printf("我是进程%d...",getpid());
		close(fd[0]);
		printf("请输入:\n");
		gets(buf);
		write(fd[1],buf,sizeof(buf));
		pause();		
		
	}
	else
	{
		getchar();
		close(fd[1]);
		bzero(buf,sizeof(buf));
		printf("我是子进程%d,我的父进程是%d...\n",getpid(),getppid());
		read(fd[0],buf,20);
		printf("我读到了%s\n",buf);
		kill(getppid(),2);
		
	}
}

此程序是一个简单的通过无名管道实现进程间的通信的程序!

三、共享内存
    1、由内存维护一个共享的内存区域,其它进程把自己的虚拟地址映射到这块内存,然后多个进程之间就可以共享这块内存了。
    2、这种进程间通信的好处是不需要信息复制,是进程间通信最快的一种方式。
    3、但这种通信方式会面临同步的问题,需要与其它通信方式配合,最合适的就是信号。
    
    共享内存的编程模式:
        1、进程之间要约定一个键值
        进程A        进程B    
        创建共享内存        
        加载共享内存    加载共享内存
        卸载共享内存    卸载共享内存
        销毁共享内存
    
    int shmget(key_t key, size_t size, int shmflg);
    功能:创建共享内存
    size:共享的大小,尽量是4096的位数
    shmflg:IPC_CREAT|IPC_EXCL
    返回值:IPC对象标识符(类似文件描述符)
    
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    功能:加载共享内存(进程的虚拟地址与共享的内存映射)
    shmid:shmget的返回值
    shmaddr:进程提供的虚拟地址,如果为NULL,操作系统会自动选择一块地址映射。
    shmflg:
        SHM_RDONLY:限制内存的权限为只读
        SHM_REMAP:映射已经存的共享内存。
        SHM_RND:当shmaddr为空时自动分配
        SHMLBA:shmaddr的值不能为空,否则出错
    返回值:映射后的虚拟内存地址
        
    int shmdt(const void *shmaddr);
    功能:卸载共享内存(进程的虚拟地址与共享的内存取消映射关系)
    
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    功能:控制/销毁共享内存
    cmd:
        IPC_STAT:获取共享内存的属性
        IPC_SET:设置共享内存的属性
          IPC_RMID:删除共享内存
    buf:
        记录共享内存属性的对象

例子:

程序A

#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<signal.h>
char* buf=NULL;

void sigint(int num)
{
	printf("\r接收到数据:%s\n",buf);
	printf(">");
	fflush(stdout);
}

int main()
{
	signal(SIGINT,sigint);

	key_t key=39242236;

	int pid=0;
	printf("我是进程:%d\n",getpid());
	printf("与我通信的进程是:");
	scanf("%d",&pid);
	getchar();

	int shmid=shmget(key,4096,IPC_CREAT|0744);
	if(0>shmid)
	{
		perror("shmget");
		return -1;
	}

	buf = shmat(shmid,NULL,SHM_RND);
	while(1)
	{
		printf("请输入要发送给进程%d的内容:\n",pid);
		gets(buf);
		kill(pid,SIGINT);
	}
	shmdt(buf);
}

程序B:

#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<unistd.h>
#include<signal.h>
char* buf=NULL;

void sigint(int num)
{
	printf("\r接收到数据:%s\n",buf);
	printf(">");
	fflush(stdout);
}

int main()
{
	signal(SIGINT,sigint);

	key_t key=39242236;

	int pid=0;
	printf("我是进程:%d\n",getpid());
	printf("与我通信的进程是:");
	scanf("%d",&pid);
	getchar();

	int shmid=shmget(key,4096,0);
	if(0>shmid)
	{
		perror("shmget");
		return -1;
	}

	buf = shmat(shmid,NULL,SHM_RND);
	while(1)
	{
		printf("请输入要发送给进程%d的内容:\n",pid);
		gets(buf);
		kill(pid,SIGINT);
	}
	shmdt(buf);
}

上面两个程序分别运行得到进程A和进程B

通过获取进程id用kill函数来发送信号,从而实现A和B的通信,两个程序通过共享内存通信       
四、消息队列
    1、消息队列是一个由系统内核负责存储和管理、并通过IPC对象标识符获取的数据链表。
    
    int msgget(key_t key, int msgflg);
    功能:创建或获取消息队列
    msgflg:
        创建:IPC_CREAT|IPC_EXEC
        获取:0
        
    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    功能:向消息队列发送消息
    msqid:msgget的返回人值
    msgp:消息(消息类型+消息内容)的首地址
    msgsz:消息内存的长度(不包括消息类型)
    msgflg:
        MSG_NOERROR:当消息的实际长比msgsz还要长的话,
            则按照msgsz长度截取再发送,否则产生错误。
            
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
    功能:从消息队列接收消息
    msgp:存储消息的缓冲区
    msgsz:要接收的消息长度
    msgtyp:消息的的类型(它包含消息的前4个字节)
    msgflg:
        MSG_NOWAIT:如果要接收的消息不存在,直接返回。
            否则阻塞等待。
        MSG_EXCEPT:从消息队列中接收第一个不msgtyp类型的第一个消息。
        
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    功能:控制/销毁消息队列
    cmd:
        IPC_STAT:获取消息队的属性
        IPC_SET:设置消息队列的属性
        IPC_RMID:删除消息队列

例子:

A程序

#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
//定义消息
typedef struct Msg
{
	long type;
	char buf[255];
}Msg;

int main()
{
	key_t key=ftok(".",1);
	int msgid=msgget(key,0777|IPC_CREAT);
	if(0>msgid)
	{
		perror("msgget");
		return -1;
	}
	while(1)
	{
		Msg msg={};
		msg.type=1;
		printf("请输入发送到消息队列中的内容:\n");
		gets(msg.buf);
		msgsnd(msgid,&msg,sizeof(msg.buf),0);
		//当发送消息首字母为q退出
		if('q'==msg.buf[0]) break;
		msgrcv(msgid,&msg,sizeof(msg.buf),2,0);
		printf("接收到:%s\n",msg.buf);
		//当接收消息首字母为q退出
		if('q'==msg.buf[0]) break;
		

	}
}

B程序

#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
//定义消息
typedef struct Msg
{
	long type;
	char buf[255];
}Msg;

int main()
{
	key_t key=ftok(".",1);
	int msgid=msgget(key,0);
	if(0>msgid)
	{
		perror("msgget");
		return -1;
	}
	while(1)
	{
		Msg msg={};
		msgrcv(msgid,&msg,sizeof(msg.buf),1,0);
		printf("接收到:%s\n",msg.buf);
		//当接收消息首字母为q退出
		if('q'==msg.buf[0]) break;
		printf("请输入发送到消息队列中的内容:\n");
		gets(msg.buf);
		msg.type=2;
		msgsnd(msgid,&msg,sizeof(msg.buf),0);
		//当发送消息首字母为q退出
		if('q'==msg.buf[0]) break;
		

	}
}

进程A和B通过消息队列完成IPC

五、信号量

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

1、特点

  1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

  2. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

  3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

  4. 支持信号量组。

2、原型

最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。

复制代码

1 #include <sys/sem.h>
2 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
3 int semget(key_t key, int num_sems, int sem_flags);
4 // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
5 int semop(int semid, struct sembuf semoparray[], size_t numops);  
6 // 控制信号量的相关信息
7 int semctl(int semid, int sem_num, int cmd, ...);

复制代码

semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。

semop函数中,sembuf结构的定义如下:

复制代码

1 struct sembuf 
2 {
3     short sem_num; // 信号量组中对应的序号,0~sem_nums-1
4     short sem_op;  // 信号量值在一次操作中的改变量
5     short sem_flg; // IPC_NOWAIT, SEM_UNDO
6 }

复制代码

其中 sem_op 是一次操作中的信号量的改变量:

  • sem_op > 0,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则换行它们。

  • sem_op < 0,请求 sem_op 的绝对值的资源。

    • 如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。
    • 当相应的资源数不能满足请求时,这个操作与sem_flg有关。
      • sem_flg 指定IPC_NOWAIT,则semop函数出错返回EAGAIN
      • sem_flg 没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
        1. 当相应的资源数可以满足请求,此信号量的semncnt值减1,该信号量的值减去sem_op的绝对值。成功返回;
        2. 此信号量被删除,函数smeop出错返回EIDRM;
        3. 进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt值减1,函数semop出错返回EINTR
  • sem_op == 0,进程阻塞直到信号量的相应值为0:

    • 当信号量已经为0,函数立即返回。
    • 如果信号量的值不为0,则依据sem_flg决定函数动作:
      • sem_flg指定IPC_NOWAIT,则出错返回EAGAIN
      • sem_flg没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
        1. 信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;
        2. 此信号量被删除,函数smeop出错返回EIDRM;
        3. 进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR

semctl函数中的命令有多种,这里就说两个常用的:

  • SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
  • IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。

3、例子

复制代码

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<sys/sem.h>
  4 
  5 // 联合体,用于semctl初始化
  6 union semun
  7 {
  8     int              val; /*for SETVAL*/
  9     struct semid_ds *buf;
 10     unsigned short  *array;
 11 };
 12 
 13 // 初始化信号量
 14 int init_sem(int sem_id, int value)
 15 {
 16     union semun tmp;
 17     tmp.val = value;
 18     if(semctl(sem_id, 0, SETVAL, tmp) == -1)
 19     {
 20         perror("Init Semaphore Error");
 21         return -1;
 22     }
 23     return 0;
 24 }
 25 
 26 // P操作:
 27 //    若信号量值为1,获取资源并将信号量值-1 
 28 //    若信号量值为0,进程挂起等待
 29 int sem_p(int sem_id)
 30 {
 31     struct sembuf sbuf;
 32     sbuf.sem_num = 0; /*序号*/
 33     sbuf.sem_op = -1; /*P操作*/
 34     sbuf.sem_flg = SEM_UNDO;
 35 
 36     if(semop(sem_id, &sbuf, 1) == -1)
 37     {
 38         perror("P operation Error");
 39         return -1;
 40     }
 41     return 0;
 42 }
 43 
 44 // V操作:
 45 //    释放资源并将信号量值+1
 46 //    如果有进程正在挂起等待,则唤醒它们
 47 int sem_v(int sem_id)
 48 {
 49     struct sembuf sbuf;
 50     sbuf.sem_num = 0; /*序号*/
 51     sbuf.sem_op = 1;  /*V操作*/
 52     sbuf.sem_flg = SEM_UNDO;
 53 
 54     if(semop(sem_id, &sbuf, 1) == -1)
 55     {
 56         perror("V operation Error");
 57         return -1;
 58     }
 59     return 0;
 60 }
 61 
 62 // 删除信号量集
 63 int del_sem(int sem_id)
 64 {
 65     union semun tmp;
 66     if(semctl(sem_id, 0, IPC_RMID, tmp) == -1)
 67     {
 68         perror("Delete Semaphore Error");
 69         return -1;
 70     }
 71     return 0;
 72 }
 73 
 74 
 75 int main()
 76 {
 77     int sem_id;  // 信号量集ID
 78     key_t key;  
 79     pid_t pid;
 80 
 81     // 获取key值
 82     if((key = ftok(".", 'z')) < 0)
 83     {
 84         perror("ftok error");
 85         exit(1);
 86     }
 87 
 88     // 创建信号量集,其中只有一个信号量
 89     if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1)
 90     {
 91         perror("semget error");
 92         exit(1);
 93     }
 94 
 95     // 初始化:初值设为0资源被占用
 96     init_sem(sem_id, 0);
 97 
 98     if((pid = fork()) == -1)
 99         perror("Fork Error");
100     else if(pid == 0) /*子进程*/ 
101     {
102         sleep(2);
103         printf("Process child: pid=%d\n", getpid());
104         sem_v(sem_id);  /*释放资源*/
105     }
106     else  /*父进程*/
107     {
108         sem_p(sem_id);   /*等待资源*/
109         printf("Process father: pid=%d\n", getpid());
110         sem_v(sem_id);   /*释放资源*/
111         del_sem(sem_id); /*删除信号量集*/
112     }
113     return 0;
114 }

复制代码

上面的例子如果不加信号量,则父进程会先执行完毕。这里加了信号量让父进程等待子进程执行完以后再执行。

总结:进程间通信是实现两个程序传输数据的重要手段,非常值得学习和掌握!

2017-04-19 17:41:00 baidu_33725271 阅读数 227

进程概述


一:什么是进程 ?

进程是计算机程序运行起来的动态过程。

发明计算机的目的就是为了处理各式各样的数据,人类通过编程工具(C/C++等)编写对处理数据的程序,完成了对现实世界中各类数据的抽象(数据类型),通过函数、类等方式把我们对数据处理的过程表达出来 。

程序像这样的 (C语言):
这里写图片描述

程序被转换成二进制指令序列 像这样(十六进制):
这里写图片描述

其实它在硬盘上是长这样的:
这里写图片描述

CPU把他们从外存中加载到内存里,跳到指令起始地址运行,然后流水线式(取指、译码、取数、执行、回写)工作。这个工作过程需要占用CPU资源、内存资源、数据文件资源、网络资源等。我们每运行一个程序(QQ、wechat)就是在让CPU加载存在硬盘上的二进制文件到内存中运行,他们占用的硬件资源,CPU资源,内存空间 。

所以说,进程就是执行的程序代码这个动态过程以及支持这个过程所有的资源集合 。

在Windows (Task Manager)上像这样:
这里写图片描述

在Linux(CentOs 6 Shell command: pa -A)上像这样:
这里写图片描述


二:为什么会有进程这个东西 ?

事物存在,一定是为了迎合新环境中遇到的挑战和需求,计算机科学诞生半个多世纪,从未偏离其计算这一本质。设计进程这个概念,就是计算机内部对数据处理过程的形式化。他代表这计算机正在处理数据这一客观事实,同时也便于我们对数据处理的观察和控制,真正做到是人在控制计算机 。


三:进程的学习包含哪几方面的内容 ?

序号 内容
1 进程概述
2 创建进程
3 进程运行环境
4 进程关系
5 进程调度
6 信号(软件中断)
7 进程间通信

四:如何学习进程?

1. 看书,比如 Advanced Programming in the UNIX environment (APUE) 。


2. 多在GNU/Linux环境下实际操作,动手编程,总结 。


UNIX-Linux进程管理

阅读数 34

【C语言】【unix c】进程的创建

博文 来自: weixin_38239856
没有更多推荐了,返回首页