第六章——Linux进程的详细概念和通信方式详解

目录

一、程序和进程

二、进程的创建

三、进程的销毁

四、多进程高并发设计

五、孤儿僵尸守护进程

六、进程间通信

1、信号

1.1、信号由谁产生

1.2、实例

1.3、信号的类型

1.4、信号的处理

1.5、信号的捕捉

1.6、使用signal

1.7、使用sigaction

1.8、信号的发送

2、管道

2.1、管道的创建

2.2、管道的使用

实例1:单进程使用管道进行通信

2.3、多进程使用管道进行通信

2.4、子进程使用exec启动新程序时管道的使用

2.5、关闭管道的读端/写端

2.6、练习

2.7、把管道作为标准输入和标准输出

2.8、使用popen/pclose

3、消息队列

3.1、什么是消息队列

3.2、消息队列的获取

3.3、消息队列的发送

3.4、消息的接收

3.5、消息的控制

3.6、实例

3.7、练习

4、信号量

4.1、什么是信号量

4.2、信号量的使用

4.3、实例

5、共享内存机制

5.1、两种常用共享内存方式


一、程序和进程

我们经常谈论程序,实际上就是一堆指令和数据的集合,这个集合反映在了一个静态可执行文件和相关的配置文件等,而进程是什么呢?
操作系统可以运行多个程序,那他是如何运行的?实际上,CPU的执行是很快的,而待运行的程序很多,那么为了让操作系统运行多个程序,CPU会把它的执行时间划分成很多段,比如每一段是0.1秒,那么就可以这样A程序运行0.1秒,然后B程序运行0.1,然后C程序运行0.2秒,因为这个切换很快,所以我们感觉程序是同时运行的。

从操作系统上看上面提到的运行程序就是指一个进程,因为存在切换,所以进程管理了很多资源(如打开的文件、挂起的信号、进程状态、内存地址空间等等),也就是说进程参与了CPU的调度,和管理了所有资源,哦,这句话,不是很正确,实际上现代CPU的执行非常非常快,而且操作系统有多个CPU,使用一个进程参与调度时,频繁地从CPU的寄存器和进程堆栈的保存运行状态和对应的信息都很耗时,所以现代CPU将进程仅仅作为一个资源管理的东东,而引入了线程作为CPU调度的基本单位,多个线程可以共享同一进程的所有资源(后面会讲线程)。

注意,程序并不是进程,实际上两个或多个进程不仅有可能执行同一程序,而且还有可能共享地址空间等资源。   Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。它定义在include/linux/sched.h文件中。

二、进程的创建

创建进程很简单,直接调用fork函数

#include <unistd.h>
pid_t fork(void);

创建进程用法举例

#include <unistd.h>
#include <stdio.h>
int main()
{
    pid_t fpid;//fpid表示fork函数返回的值
    int count=0;
    fpid=fork();
    if (fpid < 0)
        printf("error in fork!");
    else if (fpid == 0) {
        printf("i am the child process, my process id is %d\n",getpid());
        printf("I’m children\n");
        count +=2;
    }
    else {
        printf("i am the parent process, my process id is %d/n",getpid());
        printf("I’m parent.\n");
        count++;
    }
    printf("统计结果是: %d/n",count);
    return 0;
}

调用fork函数后,会创建一个子进程,并且父子两个进程都从fork处执行,fork函数有两个返回值,对于父进程会返回子进程的pid,此时pid会大于0,对于子进程来说,pid会等于0。   

但是,我们说过进程是内核调度资源的基本单位,那父子进程管理的资源有什么关系呢?传统的linux操作系统以统一的方式对待所有的进程:子进程复制父进程所拥有的所有资源,这种方法使得创建进程非常非常非常慢,因为子进程需要拷贝父进程的所有的地址空间,那现代的操作系统,是如何处理的呢?主要有以下三种方式:

  • 写时复制
  • 轻量级进程允许父子进程共享每进程在内核的很多数据结构,比如地址空间、打开文件表和信号处理。
  • vfork系统调用创建的进程能共享其父进程的内存地址空间,为了防止父进程重写子进程需要的数据,阻塞父进程的执行,一直到子进程退出为止。

三、进程的销毁

exit - 终止正在执行的进程
       #include <stdlib.h>
       void exit(int status);
DESCRIPTION
       The  exit()  function  causes  normal process termination and the value of status & 0377 is returned to the parent (see wait(2)).
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    pid_t fpid;//fpid表示fork函数返回的值
    int count=0;
    int status = 0;

    fpid=fork();
    if (fpid < 0)
        printf("error in fork!\n");
    else if (fpid == 0) {
        printf("i am the child process, my process id is %d\n",getpid());
        printf("I’m children\n");
        count +=2;
        exit(-10);
    }
    else {
        printf("i am the parent process, my process id is %d\n",getpid());
        printf("I’m parent.\n");
        count++;
    }
    printf("统计结果是: %d\n",count);
    //父进程捕捉子进程的状态
    wait(&status);
    printf("parent: status: %d\n", WEXITSTATUS(status));
    return 0;
}

四、多进程高并发设计

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>

typedef void (*spawn_proc_pt) (void *data);
static void worker_process_cycle(void *data);
static void start_worker_processes(int n);
pid_t spawn_process(spawn_proc_pt proc, void *data, char *name); 

int main(int argc,char **argv){
start_worker_processes(4);
//管理子进程
    wait(NULL);
}

void start_worker_processes(int n){
    int i=0;
    for(i = n - 1; i >= 0; i--){
       spawn_process(worker_process_cycle,(void *)(intptr_t) i, "worker process");
    }
}

pid_t spawn_process(spawn_proc_pt proc, void *data, char *name){

    pid_t pid;
    pid = fork();

    switch(pid){
    case -1:
        fprintf(stderr,"fork() failed while spawning \"%s\"\n",name);
        return -1;
    case 0:
          proc(data);
          return 0;
    default:
          break;
    }   
    printf("start %s %ld\n",name,(long int)pid);
    return pid;
}


static void worker_process_init(int worker){
    cpu_set_t cpu_affinity;
    //worker = 2;
	//多核高并发处理  4core  0 - 0 core 1 - 1  2 -2 3 -3  
    CPU_ZERO(&cpu_affinity);
    CPU_SET(worker % CPU_SETSIZE,&cpu_affinity);// 0 1 2 3 
    //sched_setaffinity
    if(sched_setaffinity(0,sizeof(cpu_set_t),&cpu_affinity) == -1){
       fprintf(stderr,"sched_setaffinity() failed\n");
    }
}

void worker_process_cycle(void *data){
     int worker = (intptr_t) data;
    //初始化
     worker_process_init(worker);

    //干活
    for(;;){
      sleep(10);
      printf("pid %ld ,doing ...\n",(long int)getpid());
    }
}
查看进程在cpu的核上执行的命令:
ps -eLo ruser,pid,lwp,psr,args

五、孤儿僵尸守护进程

孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。

想想我们如何模仿一个孤儿进程? 

答案是:  kill 父进程!

僵尸进程:
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

 僵尸进程怎样产生的:

   一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用 exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。

  在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装 SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了, 那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是 为什么系统中有时会有很多的僵尸进程。

怎么查看僵尸进程:

  利用命令ps,可以看到有标记为<defunct>的进程就是僵尸进程。

怎样来清除僵尸进程:

  改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用 wait,内核也会向它发送SIGCHLD消息,尽管对默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。

  把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失。

守护进程

不与任何终端关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(apache和postfix)运行,并能处理一些系统级的任务。守护进程脱离于终端,是为了避免进程在执行过程中的信息在任何终端上显示,并且进程也不会被任何终端所产生的终端信息所打断(比如关闭终端等)。那如何成为一个守护进程呢?

步骤如下:

调用fork(),创建新进程,它会是将来的守护进程.
在父进程中调用exit,保证子进程不是进程组长

调用setsid()创建新的会话区

将当前目录改成根目录(如果把当前目录作为守护进程的目录,当前目录不能被卸载他作为守护进程的工作目录)

将标准输入,标准输出,标准错误重定向到/dev/null.

我们来看这个代码:

#include <fcntl.h>
#include <unistd.h>

int daemon(int nochdir, int noclose)
{
    int fd;

    switch (fork()) {
    case -1:
        return (-1);
    case 0:
        break;
    default:
        _exit(0);
    }

    if (setsid() == -1)
        return (-1);

    if (!nochdir)
        (void)chdir("/");

    if (!noclose && (fd = open("/dev/null", O_RDWR, 0)) != -1) {
        (void)dup2(fd, STDIN_FILENO);
        (void)dup2(fd, STDOUT_FILENO);
        (void)dup2(fd, STDERR_FILENO);
        if (fd > 2)
            (void)close (fd);
    }
    return (0);
}

六、进程间通信

1、信号

什么是信号?信号是给程序提供一种可以处理异步事件的方法,它利用软件中断来实现。不能自定义信号,所有信号都是系统预定义的。

1.1、信号由谁产生

由shell终端根据当前发生的错误(段错误、非法指令等)Ctrl+c而产生相应的信号

比如:

            socket通信或者管道通信,如果读端都已经关闭,执行写操作(或者发送数据),

            将导致执行写操作的进程收到SIGPIPE信号(表示管道破裂)

该信号的默认行为:终止该进程。

在shell终端,使用kill或killall命令产生信号

1.2、实例

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


void myhandle(int sig) 
{
	printf("Catch a signal : %d\n", sig);
}

int main(void) 
{

	signal(SIGINT, myhandle);
	while (1) {
            sleep(1);
	}
	return 0;
}
 ./a.out   &
 kill  -HUP  13733     /* 向PID为13733的进程发送SIGHUP */

1.3、信号的类型

    信号名称 			说明
	-------------------------------------------
	SIGABORT		进程异常终止
	SIGALRM 	    超时告警
	SIGFPE 			浮点运算异常
	SIGHUP 			连接挂断
	SIGILL 		    非法指令
	SIGINT 			终端中断  (Ctrl+C将产生该信号)
	SIGKILL 		  *终止进程                             
	SIGPIPE 			向没有读进程的管道写数据
	SIGQUIT 			终端退出(Ctrl+\将产生该信号)
	SIGSEGV 			无效内存段访问
	SIGTERM 		终止
	SIGUSR1      *用户自定义信号1
	SIGUSR2 		 *用户自定义信号2 
	-------------------------------------->以上信号如果不被捕获,则进程接受到后都会终止!
	SIGCHLD 		子进程已停止或退出
	SIGCONT 	 *让暂停的进程继续执行
	SIGSTOP 		 *停止执行(即“暂停")
	SIGTSTP 			中断挂起
	SIGTTIN 			后台进程尝试读操作
	SIGTTOU 		后台进程尝试写
	-------------------------------------------

1.4、信号的处理

忽略此信号

捕捉信号,指定信号处理函数进行处理

执行系统默认动作,大多数都是终止进程

1.5、信号的捕捉

信号的捕获,是指,指定接受到某种信号后,去执行指定的函数。

注意:SIGKILL和SIGSTOP不能被捕获,即,这两种信号的响应动作不能被改变。

1.6、使用signal

用法:man 2 signal
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
注:signal的返回类型,和它的第二个参数,都是函数指针类型
signal的参数2可去以下特殊值:
SIG_IGN     忽略信号
SIG_DFL     恢复默认行为

实例

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

void myhandle(int sig) 
{
	printf("Catch a signal : %d\n", sig);
}

int main(void) 
{
	signal(SIGINT, myhandle);
	while (1) {
        sleep(1);
	}

	return 0;
}

  此时就不能结束该进程了!

只能通过其他终端,给该进程发送一个其他信号,使它终止 

#ps ax | grep ./a.out      //查询进程号
#kill  -HUP  进程号     

恢复信号的默认行为main3.c

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

void myhandle(int sig) 
{
	static int cnt = 0;
	printf("Catch a signal : %d\n", sig);

	signal(SIGINT, SIG_DFL); //等同于signal(sig, SIG_DFL);
}

int main(void) 
{
	signal(SIGINT, myhandle);

	while (1) {
        sleep(1);
	}

	return 0;
}

使用SIG_DFL时,仅当第一次调用自定义的行为后马上使用SIG_DFL就可恢复,如果连续捕获多次后,就不确定。

1.7、使用sigaction

sigaction与signal的区别: sigaction比signal更“健壮”,建议使用sigaction

       用法:man 2 sigaction
       结构struct sigaction
       struct sigaction {
            void (*sa_handler)(int);   /* 信号的响应函数 */
            sigset_t   sa_mask;          /* 屏蔽信号集 */                        
            int sa_flags;                /* 当sa_flags中包含 SA_RESETHAND时,接受到该信号并 
       调用指定的信号处理函数执行之后,把该信号的响应行为重置为默认行为SIG_DFL */
            ...
        }

补充:

当sa_mask包含某个信号A时,则在信号处理函数执行期间,如果发生了该信号A,则阻塞该信号A(即暂时不响应该信号),直到信号处理函数执行结束。即,信号处理函数执行完之后,再响应该信号A

实例:

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

void myhandle(int sig) 
{
	printf("Catch a signal : %d\n", sig);
}

int main(void) 
{
	struct sigaction act;

	act.sa_handler = myhandle;
	sigemptyset(&act.sa_mask);
       act.sa_flags = 0;

	sigaction(SIGINT, &act, 0);

	while (1) {
        
	}

	return 0;
}

用sigaction改变响应动作

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

void myhandle(int sig) 
{
	printf("Catch a signal : %d\n", sig);
}

int main(void) 
{
	struct sigaction act;

	act.sa_handler = myhandle;
	sigemptyset(&act.sa_mask);
	//act.sa_flags = 0;
	act.sa_flags = SA_RESETHAND;

	sigaction(SIGINT, &act, 0);

	while (1) {

	}

	return 0;
}

用sigaction恢复默认动作

1.8、信号的发送

信号的发送方式:

             在shell终端用快捷键产生信号

             使用kill,killall命令。

             使用kill函数和alarm函数

1) 使用kill函数          

给指定的进程发送指定信号
           用法:man 2 kill
           注意:
                  给指定的进程发送信号需要“权限”:
                  普通用户的进程只能给该用户的其他进程发送信号
                  root用户可以给所有用户的进程发送信号

           kill失败
                  失败时返回-1

           失败原因:
                  权限不够
                  信号不存在
                  指定的进程不存在

实例:main6.c创建一个子进程,子进程每秒中输出字符串“child process work!",父进程等待用户输入,如果用户按下字符A, 则向子进程发信号SIGUSR1, 子进程的输出字符串改为大写; 如果用户按下字符a, 则向子进程发信号SIGUSR2, 子进程的输出字符串改为小写

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

int workflag = 0;

void work_up_handle(int sig) 
{
	workflag = 1;
}

void work_down_handle(int sig) 
{
	workflag = 0;
}



int main(void) 
{
	pid_t pd;
	char c;


	pd = fork();
	if (pd == -1) {
		printf("fork error!\n");
		exit(1);
	} else if (pd == 0) {
		char *msg;
		struct sigaction act; 
		act.sa_flags = 0;
		act.sa_handler = work_up_handle;
		sigemptyset(&act.sa_mask);		
		sigaction(SIGUSR1, &act, 0);
		
		act.sa_handler = work_down_handle;
		sigaction(SIGUSR2, &act, 0);
		
		while (1) {
			if (!workflag) {
				msg = "child process work!";
			} else {
				msg = "CHILD PROCESS WORK!";
			}
			printf("%s\n", msg);
			sleep(1);
		}
		
	} else {
		while(1) {
			c = getchar();
			if (c == 'A') {
				kill(pd, SIGUSR1);
			} else if (c == 'a') {
				kill(pd, SIGUSR2);
			}
		}
	}
	

	return 0;
}

实例:main7.c “闹钟”,创建一个子进程,子进程在5秒钟之后给父进程发送一个SIGALR,父进程收到SIGALRM信号之后,“闹铃”(用打印模拟)

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

int wakeflag = 0;

void wake_handle(int sig) 
{
	wakeflag = 1;
}

int main(void) 
{
	pid_t pd;
	char c;


	pd = fork();
	if (pd == -1) {
		printf("fork error!\n");
		exit(1);
	} else if (pd == 0) {
		sleep(5);
		kill(getppid(), SIGALRM);
	} else {
		struct sigaction act; 
		act.sa_handler = wake_handle;
		act.sa_flags = 0;
		sigemptyset(&act.sa_mask);

		sigaction(SIGALRM,  &act, 0);

		pause(); //把该进程挂起,直到收到任意一个信号

		if (wakeflag) {
			printf("Alarm clock work!!!\n");
		}
	}

	return 0;
}

 2) 使用alarm函数

        作用:在指定时间之内给该进程本身发送一个SIGALRM信号。
        用法:man 2 alarm
        注意:时间的单位是“秒”
                 实际闹钟时间比指定的时间要大一点。 
                 如果参数为0,则取消已设置的闹钟。
                 如果闹钟时间还没有到,再次调用alarm,则闹钟将重新定时
                 每个进程最多只能使用一个闹钟。
        返回值:
                 失败:返回-1
                 成功:返回上次闹钟的剩余时间(秒)

实例:“闹铃”

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>

int wakeflag = 0;

void wake_handle(int sig) 
{
	wakeflag = 1;
}

int main(void) 
{
	int ret;
	
	struct sigaction act;
	act.sa_flags = 0;
	act.sa_handler = wake_handle;
	sigemptyset(&act.sa_mask);
	sigaction(SIGALRM, &act, 0);
	
	printf("time =%ld\n", time((time_t*)0));

	ret = alarm(5);
	if (ret == -1) {
		printf("alarm error!\n");
		exit(1);
	}

	//挂起当前进程,直到收到任意一个信号
	pause();

	if (wakeflag) {
		printf("wake up, time =%ld\n", time((time_t*)0));
	}

	return 0;
}

3) 使用raise

给本进程自身发送信号。      

  原型: int  raise (int sig)

发送多个信号

   某进程正在执行某个信号对应的操作函数期间(该信号的安装函数),如果此时,该进程又多次收到同一个信号(同一种信号值的信号),

则:如果该信号是不可靠信号(<32),则只能再响应一次。

如果该信号是可靠信号(>32),则能再响应多次(不会遗漏)。但是,都是都必须等该次响应函数执行完之后,才能响应下一次。

    某进程正在执行某个信号对应的操作函数期间(该信号的安装函数),如果此时,该进程收到另一个信号(不同信号值的信号),则:

   如果该信号被包含在当前信号的signaction的sa_mask(信号屏蔽集)中,则不会立即处理该信号。直到当前的信号处理函数执行完之后,才去执行该信号的处理函数。

否则:

   则立即中断当前执行过程(如果处于睡眠,比如sleep, 则立即被唤醒)而去执行这个新的信号响应。新的响应执行完之后,再在返回至原来的信号处理函数继续执行。

实例:

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

void myhandle(int sig) 
{
	printf("Catch a signal : %d\n", sig);
	int i;
	for (i=0; i<10; i++) {
		sleep(1);
}
	printf("Catch end.%d\n", sig);
}

int main(void) 
{
	struct sigaction act, act2;

	act.sa_handler = myhandle;
	sigemptyset(&act.sa_mask);
	sigaddset(&act.sa_mask, SIGUSR1);
       act.sa_flags = 0;

	sigaction(SIGINT, &act, 0);


	act2.sa_handler = myhandle;
	sigemptyset(&act2.sa_mask);
	act2.sa_flags = 0;
	sigaction(SIGUSR1, &act, 0);

	while (1) {

	}

	return 0;
}

信号集

什么是信号集

        信号集,用sigset_t类型表示,实质是一个无符号长整形。

        用来表示包含多个信号的集合。

信号集的基本操作

sigemptyset       把信号集清空
sigfillset          把所有已定义的信号填充到指定信号集
sigdelset         从指定的信号集中删除指定的信号
sigaddset        从指定的信号集中添加指定的信号     
sigismember   判断指定的信号是否在指定的信号集中
	                              如果是,    返回 1
	                              如果不是, 返回 0
	                              信号无效, 返回-1

进程的“信号屏蔽字”

进程的“信号屏蔽字”是一个信号集

想目标进程发送某信号时,如果这个信号在目标进程的信号屏蔽字中,

则目标进程将不会捕获到该信号,即不会执行该信号的处理函数。

当该进程的信号屏蔽字不再包含该信号时,则会捕获这个早已收到的信号(执行对应的函数)

修改进程的“信号屏蔽字”

使用sigprocmask
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
     how:
         SIG_BLOCK    把参数set中的信号添加到信号屏蔽字中
         SIG_UNBLOCK  把参数set中的信号从信号屏蔽字中删除
         SIG_SETMASK  把参数set中的信号设置为信号屏蔽字
         oldset
                      返回原来的信号屏蔽字

实例:

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

void myhandle(int sig) 
{
	printf("Catch a signal : %d\n", sig);
	printf("Catch end.%d\n", sig);
}

int main(void) 
{
	struct sigaction act, act2;

	act.sa_handler = myhandle;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	sigaction(SIGINT, &act, 0);

	sigset_t proc_sig_msk, old_mask;
	sigemptyset(&proc_sig_msk);
	sigaddset(&proc_sig_msk, SIGINT);

	sigprocmask(SIG_BLOCK, &proc_sig_msk, &old_mask);
	sleep(5);
	printf("had delete SIGINT from process sig mask\n");
	sigprocmask(SIG_UNBLOCK, &proc_sig_msk, &old_mask);
		
	while (1) {
		
	}

	return 0;
}
获取未处理的信号
       当进程的信号屏蔽字中信号发生时,这些信号不会被该进程响应,
       可通过sigpending函数获取这些已经发生了但是没有被处理的信号
       用法: man sigpending
       返回值:成功则返回0
                   失败则返回-1
阻塞式等待信号
       pause
            阻塞进程,直到发生任一信号后
       sigsuspend
            用指定的参数设置信号屏蔽字,然后阻塞时等待信号的发生。
            即,只等待信号屏蔽字之外的信号

2、管道

IPC 有多种方式, 管道是IPC的最基本的方式.
管道是“半双工”的,即是单向的。
管道是FIFO(先进先出)的。

单进程中的管道:
   int fd[2]
使用文件描述符fd[1], 向管道写数据
使用文件描述符fd[0], 从管道读数据

注:单进程中的管道无实际用处

        管道用于多进程间通信。

2.1、管道的创建

使用pipe系统调用
用法:见 man 2 pipe

返回值:
成功:返回  0
失败:返回 -1

注意:获取两个“文件描述符”
       分别对应管道的读端和写端。
       fd[0]: 是管道的读端
	   fd[1]: 是管道的写端
       如果对fd[0]进行写操作,对fd[1]进行读操作,可能导致不可预期的错误。

2.2、管道的使用

实例1:单进程使用管道进行通信

注意:创建管道后,获得该管道的两个文件描述符,

不需要普通文件操作中的open操作

如图:

实例:

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

int main(void) 
{
	int fd[2];
	int ret;
	char buff1[1024];
	char buff2[1024];

	ret = pipe(fd);
	if (ret !=0) {
		printf("create pipe failed!\n");
		exit(1);
	}

	strcpy(buff1, "Hello!");
	write(fd[1], buff1, strlen(buff1)); 
	printf("send information:%s\n", buff1);

	bzero(buff2, sizeof(buff2));
	read(fd[0], buff2, sizeof(buff2));
	printf("received information:%s\n", buff2);

	return 0;	
}

2.3、多进程使用管道进行通信

 注意:创建管道之后,再创建子进程,此时一共有4个文件描述符。

       4个端口,父子进程分别有一个读端口和一个写端口

       向任意一个写端口写数据,即可从任意一个读端口获取数据。

实例:

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

int main(void) 
{
	int fd[2];
	int ret;
	char buff1[1024];
	char buff2[1024];
	pid_t pd;

	ret = pipe(fd);
	if (ret !=0) {
		printf("create pipe failed!\n");
		exit(1);
	}

	pd = fork();
	if (pd == -1) {
		printf("fork error!\n");
		exit(1);
	} else if (pd == 0) {
		bzero(buff2, sizeof(buff2));
		read(fd[0], buff2, sizeof(buff2));
		printf("process(%d) received information:%s\n", getpid(), buff2);
	} else {
		strcpy(buff1, "Hello!");
		write(fd[1], buff1, strlen(buff1)); 
		printf("process(%d) send information:%s\n", getpid(), buff1);
	}

	if (pd > 0) {
		wait();
	}
	
	return 0;	
}

2.4、子进程使用exec启动新程序时管道的使用

有程序P1, P2

              使用管道进行通信

              P1由用户输入一个字符串,然后把该字符串发给p2

              P2接收到以后,把该字符串打印出来

P1:

       创建管道

       创建子进程

       在子进程中用exec替换成p2,

       (在使用exec 时,把管道的读端作为exec的参数)

       在父进程中,获取用户的输入,然后把所输入的字符串发送给p2

       (即,父进程把字符串写入管道)

P2:

        从参数中获取管道的读端(参数即为p2的main函数的参数)

        读管道

        把读到的字符串打印出来

难点:子进程使用exec启动新程序运行后,

新进程能够使用原来子进程的管道(因为exec能共享原来的文件描述符)

但问题是新进程并不知道原来的文件描述符是多少!

解决方案:

      把子进程中的管道文件描述符,用exec的参数传递给新进程。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(void) 
{
	int fd[2];
	int ret;
	char buff1[1024];
	char buff2[1024];
	pid_t pd;

	ret = pipe(fd);
	if (ret !=0) {
		printf("create pipe failed!\n");
		exit(1);
	}

	pd = fork();
	if (pd == -1) {
		printf("fork error!\n");
		exit(1);
	} else if (pd == 0) {
		//bzero(buff2, sizeof(buff2));
		sprintf(buff2, "%d", fd[0]);
		execl("main3_2", "main3_2", buff2, 0);
		printf("execl error!\n");
		exit(1);
	} else {
		strcpy(buff1, "Hello!");
		write(fd[1], buff1, strlen(buff1)); 
		printf("process(%d) send information:%s\n", getpid(), buff1);
	}

	if (pd > 0) {
		wait();
	}
	
	return 0;	
}
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[]) 
{
	int fd;
	char buff[1024] = {0,};

	sscanf(argv[1], "%d", &fd);
	read(fd, buff, sizeof(buff));

	printf("Process(%d) received information:%s\n",  getpid(), buff);	
	return 0;	
}

2.5、关闭管道的读端/写端

管道关闭后的读操作:

问题:

对管道进行read时,如果管道中已经没有数据了,此时读操作将被“阻塞”。

如果此时管道的写端已经被close了,则写操作将可能被一直阻塞!

而此时的阻塞已经没有任何意义了。(因为管道的写端已经被关闭,即不会再写入数据了)

解决方案:

如果不准备再向管道写入数据,则把该管道的所有写端都关闭,

则,此时再对该管道read时,就会返回0,而不再阻塞该读操作。(管道的特性)

注意,这是管道的特性。

      如果有多个写端口,而只关闭了一个写端,那么无数据时读操作仍将被阻塞。

实际实现方式:

父子进程各有一个管道的读端和写端;

把父进程的读端(或写端)关闭;

把子进程的写端(或读端)关闭;

使这个“4端口”管道变成单向的“2端口”管道,如图:

实例:

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

int main(void) 
{
	int fd[2];
	int ret;
	char buff1[1024];
	char buff2[1024];
	pid_t pd;

	ret = pipe(fd);
	if (ret !=0) {
		printf("create pipe failed!\n");
		exit(1);
	}

	pd = fork();
	if (pd == -1) {
		printf("fork error!\n");
		exit(1);
	} else if (pd == 0) {
		close(fd[1]);
		bzero(buff2, sizeof(buff2));
		read(fd[0], buff2, sizeof(buff2));
		printf("process(%d) received information:%s\n", getpid(), buff2);
	} else {
		strcpy(buff1, "Hello!");
		close (fd[0]);
		write(fd[1], buff1, strlen(buff1)); 
		printf("process(%d) send information:%s\n", getpid(), buff1);
		
		close (fd[1]);		
	}

	if (pd > 0) {
		wait();
	}
	
	return 0;	
}

把父进程的写操作注释掉,此时子进程的读操作将被一直阻塞

把父进程的写操作注释掉,并close父进程的写端

此时子进程的读操作将将被阻塞。

把父进程的写操作注释掉,并把父子进程的写端都close

此时子进程读操作将直接返回0,而不再阻塞。

最终实现方案:

关闭父进程的读端,关闭子进程的写端。

当父进程不再发送数据时,就关闭本进程的写端。

2.6、练习

创建一个子进程

父进程:

循环等待用户输入,

用户每输入一个单词后,就把该单词用管道发送给子进程,

直到用户输入exit。

子进程:

      每收到一个单词后,就打印输出

      直到用户在父进程中结束输入。

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

#define CMD_EXIT  "exit"

int main(void) 
{
	int fd[2];
	int ret;
	char buff[80];
	pid_t pd;

	ret = pipe(fd);
	if (ret !=0) {
		printf("create pipe failed!\n");
		exit(1);
	}

	pd = fork();
	if (pd == -1) {
		printf("fork error!\n");
		exit(1);
	} else if (pd == 0) {
		close(fd[1]);

		while (1) {
			bzero(buff, sizeof(buff));
			if (read(fd[0], buff, sizeof(buff)) == 0) {
				break;
			}
			printf("received information:%s\n", buff);
		}
		printf("receive end!\n");
		close(fd[0]);
	} else {
		close (fd[0]);

		while(1) {
			scanf("%s", buff);
			if (strcmp(buff, CMD_EXIT) == 0) {
				break;
			}

			write(fd[1], buff, strlen(buff));
		}		
		
		close (fd[1]);		
	}

	if (pd > 0) {
		wait();
	}
	
	return 0;	
}

2.7、把管道作为标准输入和标准输出

把管道作为标准输入和标准输出的优点:

子进程使用exec启动新程序时,就不需要再把管道的文件描述符传递给新程序了。

可以直接使用使用标准输入(或标准输出)的程序。

比如 od –c (统计字符个数,结果为八进制)

实现原理:

使用dup复制文件描述符

用exec启动新程序后,原进程中已打开的文件描述符仍保持打开,即可以共享原进程中的文件描述符。

注意:dup的用法

dup复制文件描述符,

返回的新文件描述符和被复制的文件描述符,指向同一个文件或管道

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(void) 
{
	int fd[2];
	int ret;
	char buff1[1024];
	char buff2[1024];
	pid_t pd;

	ret = pipe(fd);
	if (ret !=0) {
		printf("create pipe failed!\n");
		exit(1);
	}

	pd = fork();
	if (pd == -1) {
		printf("fork error!\n");
		exit(1);
	} else if (pd == 0) {
		//bzero(buff2, sizeof(buff2));
		//sprintf(buff2, "%d", fd[0]);
		close(fd[1]);

		close(0);
		dup(fd[0]);
		close(fd[0]);
		
		execlp("./od.exe", "./od.exe", "-c", 0);
		printf("execl error!\n");
		exit(1);
	} else {
		close(fd[0]);
	
		strcpy(buff1, "Hello!");
		write(fd[1], buff1, strlen(buff1)); 

		close(fd[1]);
	}
	
	return 0;	
}

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

int main(void)
{       int ret = 0;
        char buff[80] = {0,};

        ret = scanf("%s", buff);
        printf("[ret: %d]buff=%s\n", ret, buff);

        ret = scanf("%s", buff);
        printf("[ret: %d]buff=%s\n", ret, buff);
        return 0;
}

2.8、使用popen/pclose

popen的作用:

用来在两个程序之间传递数据:
在程序A中使用popen调用程序B时,有两种用法:
程序A读取程序B的输出(使用fread读取)
程序A发送数据给程序B,以作为程序B的标准输入。(使用fwrite写入)
用法:man  popen
返回值:成功,返回FILE*
        失败, 返回空

实例1:

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

#define BUFF_SIZE   1024

int main(void)
{
	FILE * file;
	char buff[BUFF_SIZE+1];
	int cnt;

	// system("ls -l > result.txt");
	file = popen("ls -l", "r");
	if (!file) {
		printf("fopen failed!\n");
		exit(1);
	}

	cnt = fread(buff, sizeof(char), BUFF_SIZE, file);
	if (cnt > 0) {
		buff[cnt] = '\0';
		printf("%s", buff);
	}	

	pclose(file);

	return 0;	
}

实例2:

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

#define BUFF_SIZE   1024

int main(void)
{
	FILE * file;
	char buff[BUFF_SIZE+1];
	int cnt;

	file = popen("./p2", "w");
	if (!file) {
		printf("fopen failed!\n");
		exit(1);
	}

	strcpy(buff, "hello world!");
	cnt = fwrite(buff, sizeof(char), strlen(buff), file);
	
	pclose(file);

	return 0;	
}

popen的原理:

先使用fork创建一个子进程,

然后在子进程中使用exec执行指定外部程序,并返回一个文件指针FILE*给父进程。

当使用”r”时,该FILE指向外部程序的标准输出

当使用”w”时,该FILE指向外部程序的标准输入。

popen的优缺点:

优点:可以使用shell扩展(比如命令中可以使用通配符)

      使用方便。

缺点:每调用一次popen, 将要启动两个进程(shell和被指定的程序),

      资源消耗大。

如果所有管道写端对应的文件描述符被关闭,则read返回0

如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE

3、消息队列

3.1、什么是消息队列

消息队列,用于从一个进程向另一个进程发送数据。

但仅把数据发送到一个“队列”中,而不指定由哪个进程来接受。

消息队列,独立与发送消息的进程和接收消息的进程。

(信号、管道、命名管道都不独立与发送和接收进程)

消息队列,有最大长度限制:MSGMNB

消息队列中的单条消息,也有最大长度限制:MSGMAX

3.2、消息队列的获取

   msgget
   原型:int  msgget(key_t key,  int msgflg);
   功能:获取或创建一个消息队列
   参数:与共享内存相似。
            msgflag可使用IPC_CREAT
   返回值:成功,返回正整数,即“消息队列标识符”
              失败,返回-1

3.3、消息队列的发送

   msgsnd
   原型: int  msgsnd(int msqid,   const void *msgp,   size_t msgsz,   int msgflg);
   功能:发送一个消息,即把消息添加到消息队列中
   参数:msgid  消息队列标识符
         msgp    消息指针
   注:消息的类型需要自己定义。但要求其第一个结构成员为long int
   例: struct  my_msg_st 
       {
          long  int  msg_type;    /* 消息的类型,取>0, 接收消息时可使用该值 */
                      				/*other info */	
       } 
   msgsz   消息的长度(不包含第一个成员msg_type)
   msgflg  如果包含:  IPC_NOWAIT, 则消息队列满时,不发送该消息,而立即返回-1
           如果不包含:IPC_NOWAIT,则消息队列满时,挂起本进程,直到消息队列有空间可用。
   返回值:成功,返回0
           失败,返回-1

3.4、消息的接收

msgrcv
    原型: ssize_t msgrcv (int msqid,  void *msgp,   size_t msgsz,   long msgtype,   int msgflg);
    功能:从消息队列中接收一条消息。
    参数:msgid  消息队列标识符
            msgp    用于接收消息的缓存
            msgsz   要接收的消息的长度(不包括其第一个成员)
            msgtype  指定接收消息的类型
                  0: 从消息队列中获取第一个消息,以实现顺序接受(先发先收)
                 >0: 从消队列中获取相同类型的第一个消息
                 <0: 从消息队列中获取消息类型<=(msgtyep的绝对值)的第一个消息
            msgflg: 如果包含   IPC_NOWAIT, 则当消息队列中没有指定类型的消息时,立即返回-1
                   如果不包含:IPC_NOWAIT,则当消息队列中没有指定类型的消息时,挂起本进程,直到收到指定类型的消息
    返回值:成功,返回接收到的消息的长度(不包含第一个成员msg_type)
               失败,返回-1

3.5、消息的控制

    msgctl
    原型:int  msgctl(int msqid,  int cmd,  struct msqid_ds *buf);
    功能:与shmctl类似
    参数:cmd 常用命令:
             IPC_RMID   删除消息队列
    返回值:成功, 返回 0
               失败,返回-1

3.6、实例

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MSG_SIZE 80

struct my_msg_st {
	long int msg_type;
	char msg[MSG_SIZE];
};

int main(void)
{
	int msgid;
	int ret;
	struct my_msg_st msg;

	msgid = msgget((key_t)1235, 0666|IPC_CREAT);
	if (msgid == -1) {
		printf("msgget failed!\n");
		exit(1);
	}

	msg.msg_type = 1;	
	strcpy(msg.msg, "Hello world!");
	ret = msgsnd(msgid, &msg, MSG_SIZE, 0);
	if (ret == -1) {
		printf("msgsnd failed!\n");
		exit(1);
	}

	return 0;
}


#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

#define MSG_SIZE 80

struct my_msg_st {
	long int msg_type;
	char msg[MSG_SIZE];
};

int main(void)
{
	int msgid;
	int ret;
	struct my_msg_st msg;

	msgid = msgget((key_t)1235, 0666|IPC_CREAT);
	if (msgid == -1) {
		printf("msgget failed!\n");
		exit(1);
	}

	msg.msg_type = 0;	
	ret = msgrcv(msgid, &msg, MSG_SIZE, 0, 0);
	if (ret == -1) {
		printf("msgrcv failed!\n");
		exit(1);
	}

	printf("received: %s\n", msg.msg);

	ret = msgctl(msgid, IPC_RMID, 0);
	if (ret == -1) {
		printf("msgctl(IPC_RMID) failed!\n");
		exit(1);
	}

	return 0;
}

3.7、练习

 程序1, 循环等待用户输入字符串,

                每收到一个字符串,就把它发送给进程2

                直到用户输入exit

    程序2, 接受进程1发过来的信息,并打印输出。

                直到接受到exit。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MSG_SIZE 80

struct my_msg_st {
	long int msg_type;
	char msg[MSG_SIZE];
};

int main(void)
{
	int msgid;
	int ret;
	struct my_msg_st msg;

	msgid = msgget((key_t)1235, 0666|IPC_CREAT);
	if (msgid == -1) {
		printf("msgget failed!\n");
		exit(1);
	}

	while(1) {
		fgets(msg.msg, sizeof(msg.msg), stdin);
			
		msg.msg_type = 1;	
		ret = msgsnd(msgid, &msg, MSG_SIZE, 0);
		if (ret == -1) {
			printf("msgsnd failed!\n");
			exit(1);
		}

		if (strncmp(msg.msg, "exit", 4) == 0) {
			break;
		}
	}

	return 0;
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

#define MSG_SIZE 80

struct my_msg_st {
	long int msg_type;
	char msg[MSG_SIZE];
};

int main(void)
{
	int msgid;
	int ret;
	struct my_msg_st msg;

	msgid = msgget((key_t)1235, 0666|IPC_CREAT);
	if (msgid == -1) {
		printf("msgget failed!\n");
		exit(1);
	}

	while(1) {
		msg.msg_type = 0;	
		ret = msgrcv(msgid, &msg, MSG_SIZE, 0, 0);
		if (ret == -1) {
			printf("msgrcv failed!\n");
			exit(1);
		}

		printf("received: %s\n", msg.msg);

		if (strncmp(msg.msg, "exit", 4) == 0) {
			break;
		}
	}

	ret = msgctl(msgid, IPC_RMID, 0);
	if (ret == -1) {
		printf("msgctl(IPC_RMID) failed!\n");
		exit(1);
	}

	return 0;
}

4、信号量

问题

程序中,有时存在一种特殊代码,最多只允许一个进程执行该部分代码。这部分区域,称为“临界区”

然而在多进程并发执行时,当一个进程进入临界区,因某种原因被挂起时,其他进程就有可能也进入该区域。

解决办法:使用信号量

4.1、什么是信号量

信号量,是一种特殊的变量。

    只能对信号量执行P操作和V操作

    P操作, 如果信号量的值 > 0,    则把该信号量减1

               如果信号量的值  ==0,  则挂起该进程。

    V操作:  如果有进程因该信号量而被挂起,则恢复该进程运行

               如果没有进程因该信号量而挂起,则把该信号量加1

    注意:P操作、V操作都是原子操作,即其在执行时,不会被中断。

    注意:此指的“信号量”是指System V  IPC的信号量,与线程所使用的信号量不同。该信号量,用于进程间通信

4.2、信号量的使用

   1)信号量的获取 
   semget
        
 原型:int semget(key_t key,   int nsems,   int semflg);
 功能:获取一个已存在的、或创建一个新的信号量量,返回该信号量的标识符
 参数:key, 键值,该键值对应一个唯一的信号量。类似于共享内存的键值。
      不同的进程可通过该键值和semget获取唯一的信号量。特殊键值:IPC_PRIVAT该信号量只允许创建者本身, 可用于父子进程间通信。

nsems, 需要的信号量数目,一般取1

sem_flags, 与共享内存的sem_flags类似。IPC_CREAT, 如果该信号量未存在,则创建该信号量如果该信号量已存在,也不发送错误。

  返回值: 成功,则返回一个正数
           失败,   返回返回-1
    
   2) 信号量的操作 
   semop
       
       原型:int semop(int semid,   struct sembuf *sops,   unsigned nsops);
       功能:改变信号量的值,即对信号量执行P操作、或V操作。
       参数:semid, 信号量标识符, 即semget的返回值
             sops,  是一个数组,元素类型为struct sembuf
                          
    struct sembuf {
         short  sem_num;  //信号量组中的编号(即指定对哪个信号量操作)
                         //semget实际是获取一组信号量
                         //如果只获取了一个信号量,则该成员取0
         short  sem_op;     //   -1,  表示P操作
                           //   1,  表示V操作
         short  sem_flg;     // SEM_UNDO : 如果进程在终止时,没有释放信号量
                           // 如果不设置指定标志,应该设置为0                                 则,自动释放该信号量                     
     }        
               
      nsops, 表示第二个参数sops所表示的数组的大小,即表示有几个struct sembuf
       
       返回值: 失败, 返回-1
                成功, 返回 0
                   
   3) 信号量的控制
       semctl
       原型:int  semctl(int semid,  int sem_num,  int cmd,  ...);
       功能:对信号量进行控制
       参数:semid, 信号量标识符
             sem_num, 信号量组中的编号,如果只有一个信号量,则取0
             cmd, SETVAL   把信号量初始化为指定的值,具体的值由第4个参数确定
       注意:只能对信号量初始化一次,如果在各进程中,分别对该信号量进行初始化,则可能导致错误!
             IPC_RMID 删除信号量
                
参数4, 类型为:union  semun {
                 int     val;      // SETVAL命令要设置的值
                 struct  semid_ds  *buf;
                 unsigned short    *array;
               }
注意:union  semun类型要求自己定义有些Linux发行版在sys/sem.h中定义,有些发行版则没有定义。
                    
                 可自定义如下:
-------------------------------------------------------------------------------------
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)						   
						#else
						    union semun {
						        int val;                             
						        struct semid_ds *buf;    
						        unsigned short int *array; 
						        struct seminfo *__buf;  
						    };
						#endif     
					

4.3、实例

实例1:不使用信号量,并发执行多个进程,观察对临界区的访问。

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

int main(void) 
{
	int i;
pid_t pd = fork();
	for (i=0; i<5; i++) {
		
		/* 模拟临界区----begin */
		printf("Process(%d) In\n", getpid());		
		sleep(1);
		printf("Process(%d) Out\n", getpid());
              /* 模拟临界区----end */ 
  
		sleep(1);
	}

	return 0;
}

实例2:使用信号量,并发执行多个进程,观察对临界区的访问。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <stdio.h>

#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)						   
#else
    union semun {
        int val;                             
        struct semid_ds *buf;    
        unsigned short int *array; 
        struct seminfo *__buf;  
    };
#endif     

static sem_initial(int semid) 
{
	int ret;
	
	union semun semun;
	semun.val = 1;
	ret = semctl(semid, 0, SETVAL, semun);
	if (ret == -1) {
		fprintf(stderr, "semctl failed!\n");
	}
	
	return ret;
}

static int  sem_p(int semid)
{
	int ret;
	
	struct sembuf sembuf;
	sembuf.sem_op = -1;
	sembuf.sem_num = 0;
	sembuf.sem_flg = SEM_UNDO;
	ret = semop(semid, &sembuf, 1);	
	if (ret == -1) {
		fprintf(stderr, "sem_p failed!\n");
	}
	
	return ret;
}

static int  sem_v(int semid)
{
	int ret;
	
	struct sembuf sembuf;
	sembuf.sem_op = 1;
	sembuf.sem_num = 0;
	sembuf.sem_flg = SEM_UNDO;
	ret = semop(semid, &sembuf, 1);	
	if (ret == -1) {
		fprintf(stderr, "sem_v failed!\n");
	}
	
	return ret;
}

int main(int argc, char* argv[]) 
{
	int i;
	int ret;
	int semid;

	/* 获取信号量 */
	semid = semget((key_t)1234, 1, 0666 | IPC_CREAT);
	if (semid == -1) {
		printf("semget failed!\n");
		exit(1);
	}

	/* 初始化信号量 */
	if (argc > 1) {
		ret = sem_initial(semid);
		if (ret == -1) {
			exit(1);
		}
	}

	for (i=0; i<5; i++) {

		if (sem_p(semid) == -1) {
			exit(1);
		}
		
		/* 模拟临界区----begin */
		printf("Process(%d) In\n", getpid());		
		sleep(1);
		printf("Process(%d) Out\n", getpid());
              /* 模拟临界区----end */ 

		if (sem_v(semid) == -1) {
			exit(1);
		}
			  
		sleep(1);
	}

    /* 删除信号量 */
	   
	return 0;
}

5、共享内存机制

是允许两个或多个进程(不相关或有亲缘关系)访问同一个逻辑内存的机制。它是共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。

5.1、两种常用共享内存方式

System V版本的共享内存 shmm    

多个进程直接共享内存

文件映射 mmap

文件进行频繁读写,将一个普通文件映射到内存中

将特殊文件进行匿名内存映射,为关联进程提供共享内存空间

为无关联的进程提供共享内存空间,将一个普通文件映射到内存中

ftok函数生成key标识符

key_t ftok(const char *pathname,int proj_id)

创建一个共享内存块,返回这个共享内存块的标识符shmid

int shmget(key_t key,size_t size,int shmflg)
参数说明:size   - 申请的共享内存的大小,为4k的整数倍;
          shmflg - IPC_CREAT 创建新的共享内存,已存在 使用IPC_EXCL

挂接共享内存(将进程地址空间挂接到物理空间,可以有多个挂接)

void *shmat(int shmid,const void *shmaddr, int shmflg)
参数说明:shmid   - 挂接的共享内存ID.
          shmaddr - 一般为0,表示连接到由内核选择的第一个可用地址上
          shmflg  - 一般为0

取消共享内存映射

int shmdt(const void *shmaddr);

用于控制共享内存

int shmctl(int shmid, int cmd, struct shmid_ds  *buf);
参数   shmid - 由shmget返回的共享内存标识码
       cmd   - 将要采取的动作(可取值:IPC_STAT、IPC_SET、IPC_RMID)
     buf   - 指向一个保存着共享内存的模式状态和访问权限的数据结构
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>

struct Conn_stat
{
	int count;
	char ip[64];
};


int main()
{
	
	void *shm = NULL;
	
	int shmid = 0, i = 0;
	struct Conn_stat stat = {0,"127.0.0.1"};
	
	//创建共享内存
	
	shmid = shmget((key_t)1234, sizeof(struct Conn_stat), 0666|IPC_CREAT);
	if(shmid == -1)
	{
		fprintf(stderr, "shmget failed\n");
		exit(1);
	}
	//将共享内存连接到当前进程的地址空间
	shm = shmat(shmid, (void*)0, 0);
	if(shm == (void*)-1)
	{
		fprintf(stderr, "shmat failed\n");
		exit(2);
	}
	printf("Memory attached at %p\n", shm);
	
	//设置共享内存
	struct Conn_stat *p = (struct Conn_stat*)shm;
	memcpy(p,&stat,sizeof(struct Conn_stat));
	
	while((i++) < 30)//修改共享内存中写数据
	{
		p->count++;
		sleep(1);
	}
	
	//把共享内存从当前进程中分离
	if(shmdt(shm) == -1)
	{
		fprintf(stderr, "shmdt failed\n");
		exit(3);
	}
	
	exit(0);
}
//shmread.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include <errno.h>

struct Conn_stat
{
	int count;
	char ip[64];
	
};

int main()
{
	void *shm = NULL;//分配的共享内存的原始首地址
	struct Conn_stat *stat = NULL;//指向shm
	int shmid;//共享内存标识符
	
	//创建共享内存
	shmid = shmget((key_t)1234, sizeof(struct Conn_stat), 0666|IPC_CREAT);
	if(shmid == -1)
	{
		fprintf(stderr, "shmget failed\n");
		exit(0);
	}
	//将共享内存连接到当前进程的地址空间
	shm = shmat(shmid, 0, 0);
	if(shm == (void*)-1)
	{
		fprintf(stderr, "shmat failed\n");
		exit(1);
	}
	printf("\nMemory attached at %p\n", shm);
	//设置共享内存
	stat = (struct Conn_stat*)shm;
	

	int i = 0;
	while((i++) < 10)
	{
		
		printf("ip = %s ,count: %d\t\t\n", stat->ip, stat->count);	
		sleep(1);
	}
	
	//把共享内存从当前进程中分离
	if(shmdt(shm) == -1)
	{
		fprintf(stderr, "shmdt failed\n");
		exit(2);
	}
	//删除共享内存
	if(shmctl(shmid, IPC_RMID, 0) == -1)
	{
		fprintf(stderr, "shmctl(IPC_RMID) failed, reason: %s\n",strerror(errno));
		exit(3);
	}
	exit(0);
}

#include <sys/mman.h>

       void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);
       int munmap(void *addr, size_t length); 

参数addr:指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选
            定地址,映射成功后返回该地址。


参数length:代表将文件中多大的部分映射到内存。


参数prot:映射区域的保护方式。可以为以下几种方式的组合:

           PROT_EXEC         执行   
           PROT_READ         读取
           PROT_WRITE        写入
          PROT_NONE         不能存取

参数flags:影响映射区域的各种特性。必须要指定MAP_SHARED 或MAP_PRIVATE。

     MAP_SHARED     - 映射区域数据与文件对应,允许其他进程共享
     MAP_PRIVATE    - 映射区域生成文件的copy,修改不同步文件
     MAP_ANONYMOUS  - 建立匿名映射。此时会忽略参数fd,不涉及文件,而
                       且映射区域无法和其他进程共享。
     MAP_DENYWRITE  - 允许对映射区域的写入操作,其他对文件直接写入的
                       操作将会被拒绝。
     MAP_LOCKED     - 将映射区域锁定住,这表示该区域不会被置swap

参数fd:要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映射的效果。

参数offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。
#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 Conn_stat
{
	int count;
	char ip[64];
};


int main(int argc,char *argv[]) //这个进程用于创建映射区进行写。
{
	if(argc != 2)
	{
		printf("Usage: %s  file.\n",argv[0]);
		exit(1);
	}

	struct Conn_stat stat = {0,"127.0.0.1"};

	int fd = open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0644);
	if(fd < 0)
	{
		perror("open");
		exit(2);
	}
	ftruncate(fd,sizeof(struct Conn_stat)); 
	
	struct Conn_stat *p = (struct Conn_stat*)mmap(NULL,sizeof(struct Conn_stat),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);//创建一个结构体大小的共享映射区。共享映射区我们可以当做数组区看待。
	if(p == MAP_FAILED)
	{
		perror("mmap");
		exit(3);
	}
	close(fd); //关闭不用的文件描述符。

	memcpy(p,&stat,sizeof(struct Conn_stat));
	
	while(p->count < 30)
	{
		
		p->count++;
		sleep(1);
	}
	int ret = munmap(p,sizeof(struct Conn_stat));
	if(ret < 0)
	{
		perror("mmumap");
		exit(4);
	}

	return 0;
}
#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 Conn_stat
{
	int count;
	char ip[64];
};

int main(int argc,char *argv[]) //这个进程创建映射区进行读
{
	if(argc != 2)
	{
		printf("Usage: %s  file.\n",argv[0]);
		exit(1);
	}


	int fd = open(argv[1],O_RDONLY,0644);
	if(fd < 0)
	{
		perror("open");
		exit(2);
	}
	
	struct Conn_stat stat;

	struct Conn_stat *p = (struct Conn_stat*)mmap(NULL,sizeof(struct Conn_stat),PROT_READ,MAP_SHARED,fd,0);
	if(p == MAP_FAILED)
	{
		perror("mmap");
		exit(3);
	}
	close(fd);
	
	int i = 0;
	while((i++) < 10)
	{
		
		printf("ip = %s ,count: %d\t\t\n",p->ip,p->count);	
		sleep(1);
	}
	int ret = munmap(p,sizeof(stat));
	if(ret < 0)
	{
		perror("mmumap");
		exit(4);
	}

	return 0;
}

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-14 20:32:03       66 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-14 20:32:03       70 阅读
  3. 在Django里面运行非项目文件

    2024-07-14 20:32:03       57 阅读
  4. Python语言-面向对象

    2024-07-14 20:32:03       68 阅读

热门阅读

  1. cosyvocie 自回归模型忽大忽下问题解决方案

    2024-07-14 20:32:03       19 阅读
  2. k8s之client-go:golang与k8s交互的桥梁

    2024-07-14 20:32:03       22 阅读
  3. 使用harbor作为chart仓库实现内网部署

    2024-07-14 20:32:03       18 阅读
  4. uniapp使用微信登录

    2024-07-14 20:32:03       19 阅读
  5. git patch怎么使用?

    2024-07-14 20:32:03       20 阅读
  6. git 分支介绍

    2024-07-14 20:32:03       20 阅读
  7. Mybatis-plus3.4.3下使用lambdaQuery报错

    2024-07-14 20:32:03       21 阅读
  8. 模拟电路再理解系列(2)-电源滤波电路

    2024-07-14 20:32:03       22 阅读