Linux 多进程编程基础


本文将对Linux下多进程编程进行介绍。

进程的概念

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。通俗点讲,进程是一段程序的执行过程,是个动态概念。

进程的创建

进程的状态

在这里插入图片描述
程序运行必须加载在内存中,当有过多的就绪态或阻塞态进程在内存中没有运行,因为内存很小,有可能不足。系统需要把他们移动到内存外磁盘中,称为挂起状态。就绪状态的进程挂起就是挂起就绪状态,阻塞进程挂起就称为阻塞挂起状态。

进程的调度

每个进程的产生都有自己的唯一的ID号(pid),并且附带有一个它父进程的ID号(ppid)。进程死亡时,ID被回收。

进程间靠优先级获得CPU资源,时间片段轮换来更新优先级,以保证不会一个进程占据CPU时间过长。每个进程都得到轮换运行,因为这个时间非常短,所以给我们就好像是系统在同时运行好多进程。

进程的死亡

僵尸进程

僵尸进程即子进程先于父进程退出后,子进程的PCB需要其父进程释放,但是父进程并没有释放子进程的PCB,这样的子进程就称为僵尸进程,僵尸进程实际上是一个已经死掉的进程。我们用代码来看一下

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
 
int main()
{
	pid_t pid=fork();
 
	if(pid==0)  //子进程
	{
     	printf("child id is %d\n",getpid());
		printf("parent id is %d\n",getppid());
	}
	else  //父进程不退出,使子进程成为僵尸进程
	{
        while(1)
		{}
	}
	exit(0);
}

我们将它挂在后台执行,可以看到结果,用ps可以看到子进程后有一个<defunct> ,defunct是已死的,僵尸的意思,可以看出这时的子进程已经是一个僵尸进程了。因为子进程已经结束,而其父进程并未释放其PCB,所以产生了这个僵尸进程。

$ ps -aux | grep test_process
marvin    103549  100  0.0   2428  1388 pts/20   R    20:03   1:18 ./test_process
marvin    103552  0.0  0.0      0     0 pts/20   Z    20:03   0:00 [test_process] <defunct>

由于子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束. 那么会不会因为父进程太忙来不及wait子进程,或者说不知道子进程什么时候结束,而丢失子进程结束时的状态信息呢?
不会。因为UNⅨ提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息,就可以得到。这种机制就是:在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。这个僵尸进程需要它的父进程来为它收尸,如果他的父进程没有处理这个僵尸进程的措施,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。

试想一下,如果有大量的僵尸进程驻在系统之中,必然消耗大量的系统资源。但是系统资源是有限的,因此当僵尸进程达到一定数目时,系统因缺乏资源而导致奔溃。所以在实际编程中,避免和防范僵尸进程的产生显得尤为重要。

孤儿进程

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

子进程死亡需要父进程来处理,那么意味着正常的进程应该是子进程先于父进程死亡。当父进程先于子进程死亡时,子进程死亡时没父进程处理,这个死亡的子进程就是孤儿进程。

但孤儿进程与僵尸进程不同的是,由于父进程已经死亡,系统会帮助父进程回收处理孤儿进程。所以孤儿进程实际上是不占用资源的,因为它终究是被系统回收了。不会像僵尸进程那样占用ID,损害运行系统。

下来我们上代码看看:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
 
int main()
{
	pid_t pid=fork();
 
	if(pid==0)
	{
		printf("child ppid is %d\n",getppid());
		sleep(10);     //为了让父进程先结束
		printf("child ppid is %d\n",getppid());
	}
	else
	{
		printf("parent id is %d\n",getpid());
	}
 
	exit(0);
}
dev@arch-dev ~/temp> ./test_process
parent id is 71773
child ppid is 71773
dev@arch-dev ~/temp> child ppid is 1
dev@arch-dev ~/temp> cat /proc/1/cmdline 
/sbin/init

从执行结果来看,此时由pid == 71773父进程创建的子进程,其输出的父进程pid == 1,说明当其为孤儿进程时被init进程回收,最终并不会占用资源,这就是为什么要将孤儿进程分配给init进程。需要注意的是,托管孤儿进程的进程不一定总是pid为1,在我的另外一台主机上就是:

> ./test_process
parent id is 105701
child ppid is 105701
> child ppid is 1914
> cat /proc/1914/cmdline 
/usr/lib/systemd/systemd--user

僵尸进程处理方式

任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“defunct”。如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。所以孤儿进程不会占资源,僵尸进程会占用资源危害系统。我们应当避免僵尸进程的出现。

解决方式如下:

1):一种比较暴力的做法是将其父进程杀死,那么它的子进程,即僵尸进程会变成孤儿进程,由系统来回收。但是这种做法在大多数情况下都是不可取的,如父进程是一个服务器程序,如果为了回收其子进程的资源,而杀死服务器程序,那么将导致整个服务器崩溃,得不偿失。显然这种回收进程的方式是不可取的,但其也有一定的存在意义。

2):SIGCHLD信号处理

我们都知道wait函数是用来处理僵尸进程的,但是进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。我们先来看看wait函数的定义

#include <sys/types.h>   /* 提供类型pid_t的定义,实际就是int型 */

#include <sys/wait.h>

pid_t wait(int *status)

参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:pid=wait(NULL);如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

由于调用wait之后,就必须阻塞,直到有子进程结束,所以,这样来说是非常不高效的,我们的父进程难道要一直等待你子进程完成,最后才能执行自己的代码吗?难道就不能我父进程执行自己的代码,你子进程什么时候完成我就什么时候去处理你,不用一直等你?当然是有这种方式了。

实际上当子进程终止时,内核就会向它的父进程发送一个SIGCHLD信号,父进程可以选择忽略该信号,也可以提供一个接收到信号以后的处理函数。对于这种信号的系统默认动作是忽略它。我们不希望有过多的僵尸进程产生,所以当父进程接收到SIGCHLD信号后就应该调用 wait 或 waitpid 函数对子进程进行善后处理,释放子进程占用的资源。

下面是一个处理僵尸进程的简单的例子:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>
 
void deal_child(int num)
{
	printf("deal_child into\n");
	wait(NULL);
}
 
int main()
{
	signal(SIGCHLD,deal_child);
	pid_t pid=fork();
	int i;
 
	if(pid==0)
	{
		printf("child is running\n");
		sleep(2);
		printf("child will end\n");
	}
	else
	{
		sleep(1);   //让子进程先执行
		printf("parent is running\n");
		sleep(10);    //一旦被打断就不能再进入睡眠
		printf("sleep 10 s over\n");
		sleep(5);
		printf("sleep 5s over\n");
	}
 
	exit(0);
}

进行测试后确定了是在父进程睡眠10s时子进程结束,父进程接收到了SIGCHLD信号,调用了deal_child函数,释放了子进程的PCB后又回到自己本身的代码中执行。我们看看运行结果

> ./test_process
child is running
parent is running
child will end
deal_child into
sleep 10 s over
sleep 5s over

说到这里,我们再来看看signal函数(不是阻塞函数)

signal(参数1,参数2);

参数1:我们要进行处理的信号。系统的信号我们可以再终端键入 kill -l查看(共64个)。其实这些信号是系统定义的宏。

参数2:我们处理的方式(是系统默认还是忽略还是捕获)。

eg: signal(SIGINT ,SIG_ING );              //SIG_ING 代表忽略SIGINT信号

eg:signal(SIGINT,SIG_DFL);    //SIGINT信号代表由InterruptKey产生,通常是CTRL +C或者是DELETE。发送给所有ForeGroundGroup的进程。 SIG_DFL代表执行系统默认操作,其实对于大多数信号的系统默认动作是终止该进程。这与不写此处理函数是一样的。

我们也可以给参数2传递一个信号处理函数的地址,但是这个信号处理函数需要其返回值为void,并且默认自带一个int类型参数
这个int就是你所传递的第一个信号参数的值(你用kill -l可以查看)
我们测试了一下,如果创建了5个子进程,但是销毁的时候仍然有两个仍是僵尸进程,这又是为什么呢?
这是因为当5个进程同时终止的时候,内核都会向父进程发送SIGCHLD信号,而父进程此时有可能仍然处于信号处理的deal_child函数中,那么在处理完之前,中间接收到的SIGCHLD信号就会丢失,内核并没有使用队列等方式来存储同一种信号
所以为了解决这一问题,我们需要调用waitpid函数来清理子进程。

void deal_child(int sig_no)
 
{
 
    for (;;) {
 
        if (waitpid(-1, NULL, WNOHANG) == 0)
 
            break;
 
    }  
 
}

这样的话,只有检验没有僵尸进程,他才会返回0,这样就可以确保所有的僵尸进程都被杀死了。具体wait和waitpid之后会发详细的。

僵尸进程的产生、危害及避免方法

1.僵尸进程:前文已经对僵尸进程的定义进行了说明。那么defunct进程只是在process table(进程表项)里有一个记录,其他的资源没有占用,除非你的系统的process个数已经快超过限制了,zombie进程不会有更多的坏处。

2.产生原因:在子进程终止后到父进程调用wait()前的时间里,子进程被称为zombie;

具体a. 子进程结束后向父进程发出SIGCHLD信号,父进程默认忽略了它
b. 父进程没有调用wait()或waitpid()函数来等待子进程的结束
c. 网络原因有时会引起僵尸进程;

3. 危害

僵尸进程会占用系统资源,如果很多,则会严重影响服务器的性能;

孤儿进程不会占用系统资源,最终是由init进程托管,由init进程来释放;

signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号,这是一个常用于提升并发服务器性能的技巧
                          // 因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。
                          // 如果将此信号的处理方式设置为忽略,可让内核把僵尸进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。

4.如何防止僵尸进程

(1) 让僵尸进程成为孤儿进程,由init进程回收;(手动杀死父进程)

(2) 调用fork()两次,父进程fork一个子进程,然后继续工作,子进程fork一 个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。

#include <unistd.h>  
#include <stdio.h>   
int main ()   
{   
	int pid = fork();
    	if(pid < 0) {
        	printf("create process error. ret=-1"); //ret=0
        	return -1;
    	}
	if(pid > 0)  //父进程中,pid是子进程到进程号
	{
		int status = 0;
		if(waitpid(pid, &status, 0) == -1) //这里阻塞一直等待
		{
			printf("wait child process error! ret=-1"); //ret=0
		}
		printf("grandpa process exit.\n");
		exit(0);
	}

   	pid = fork();
   	if(pid < 0) {
       		printf("create process error. ret=0");
        	return -1;
    	 }

	if(pid > 0)//新到父进程退出,最开始到父进程也随即退出
	{
		printf("father process exit. ret=0");
		exit(0);
	}

    //以后的子进程交给init进程接管
    while(1)
    {
     sleep(1);
     printf("son(deamon) process running.\n");
    }
    return 0;  
}  

(3) 捕捉SIGCHLD信号,并在信号处理函数中调用wait函数;

下面给出一个具体的案例来说明这种方法。

#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
 
void sig_handler(int signo)
{
	printf("child process deaded, signo: %d\n", signo);
	wait(0);// 当捕获到SIGCHLD信号,父进程调用wait回收,避免子进程成为僵尸进程
}
 
void out(int n)
{
	int i;
	for(i = 0; i < n; ++i)
	{
		printf("%d out %d\n", getpid(), i);
		sleep(2);
	}
}
 
int main(void)
{
	// 登记一下SIGCHLD信号
	if(signal(SIGCHLD, sig_handler) == SIG_ERR)
	{
		perror("signal sigchld error");
	}
	pid_t pid = fork();
	if(pid < 0)
	{
		perror("fork error");
		exit(1);
	}
	else if(pid > 0)
	{
		// parent process
		out(100);
	}
	else
	{
		// child process
		out(10);
	}
 
	return 0;
}

在上面的信号处理函数sig_handler中我们调用了wait函数,目的是为了让父进程在捕获到子进程结束发出的SIGCHLD信号后对子进程进行回收,避免子进程成为僵尸进程。这里的wait函数不同于下面第4中方法中的wait的用法,这里只有在父进程捕获到子进程结束时才调用wait对其进行回收,其他时间父进程还是继续执行。而在方法4中,调用wait函数会发生阻塞。

(4) 让僵尸进程的父进程来回收,父进程每隔一段时间来查询子进程是否结束并回收,调用wait()或者waitpid(),通知内核释放僵尸进程;

wait函数的原型是:

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int *status);

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸进程的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个这样的进程出现为止。

参数status用来保存被回收进程退出时的一些状态,如果我们不想知道这个子进程是如何死掉的,只想把它消灭掉的话,那么我们可以设定这个参数为NULL,就像下面这样:

pid = wait(NULL);

如果成功,wait会返回被回收子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

waitpid函数的原型是:

#include <sys/types.h>

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

waitpid相比于wait函数多了两个参数,下面对这两个参数做一个详细说明。

pid

从参数的名字pid和类型pid_t 就可以看出,这里需要的是一个进程ID。当pid取不同的值时,在这里有不同的意义。

  • pid > 0时,只等待进程ID等于pid的子进程,不管其他已经有多少个子进程运行结束退出了,只要指定的子进程还没结束,waitpid就会一直等下去;
  • pid = -1时,等待任何一个子进程退出,没有 任何限制,此时和wait函数作用一样;
  • pid = 0时,等待同一个进程组中的任何子进程,如果 子进程已经加入了别的进程组,waitpid不会对它做任何理睬;
  • pid < -1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值;

options

options目前只支持WNOHANGWUNTRACED两个选项,这是两个常数,可以用“|”运算符把它们连接起来使用,比如:

ret = waitpid(-1, NULL, WNOHANG | WUNTRACED);

如果我们不想使用它们,也可以把options设为0,如:

ret = waitpid(-1, NULL, 0);

如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去;

返回值和错误

waitpid的返回值比wait稍微复杂一些,一共有三种情况。

  • 当正常返回时,waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可以收集,则返回0;(非阻塞)
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

wait和waitpid的区别:

  • 在一个子进程终止前,wait使其调用者阻塞,而waitpid则提供了非阻塞版本;
  • waitpid等待一个指定的子进程,而wait等待第一个终止的子进程;
  • waitpid支持作业控制(以WUNTRACED选项,由pid指定的任一子进程状态,且其状态自暂停以来还未报告过,则返回其状态);

举个例子:当同时有5个客户连上服务器,也就是说有5个子进程分别对应了5个客户。若此时,5个客户几乎同时请求终止,即5个FIN发现服务器,同样的,5个SIGCHLD信号到达服务器,然而,UNIX的信号往往是不会排队的,这样一来,信号处理函数将会只执行一次,残留剩余4个子进程作为僵尸进程驻留在内核空间。此时,正确的解决办法就是利用waitpid(-1, &status, WNOHANG)防止留下僵尸进程。其中的pid为-1表明等待第一个终止的子进程,而WNOHANG选择项通知内核在没有已终止进程时不要阻塞。

检查wait和waitpid两个函数返回终止状态的宏

WIFEXITED/WEXITSTATUS(status)

若为正常终止子进程返回的状态,则为真。

WIFSIGNALED/WTERMSIG(status)

若为异常终止子进程返回的状态,则为真(接到一个不能捕捉的信号)

WIFSTOPPED/WSTOPSIG(status)看当前子进程在终止前是否暂停过

若为当前暂停子进程的返回状态,则为真。

下面以一个案例来说明wait和waitpid的用法

#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
 
void out_status(int status)
{
//	printf("status: %d\n", status);
	if(WIFEXITED(status))//正常终止
	{
		printf("normal exit: %d\n", WEXITSTATUS(status));
	}
	else if(WIFSIGNALED(status))// 非正常终止
	{
		printf("abnormal term: %d\n", WTERMSIG(status));
	}
	else if(WIFSTOPPED(status))// 终止前是否暂停过
	{
		printf("stopped sig: %d\n", WSTOPSIG(status));
	}
	else//未知
	{
		printf("unknown sig\n");
	}
}
 
int main(void)
{
	int status;
	pid_t pid;
 
	// 正常终止
	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(1);
	}
	else if(pid == 0)
	{
		printf("pid: %d, ppid: %d\n", getpid(), getppid());
		exit(3);// 子进程终止运行(状态码为3也算是一种正常终止)
	}
	// 父进程调用阻塞,等待子进程结束并回收
	wait(&status);
	out_status(status);
	printf("--------------------------\n");
 
	//异常终止
	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(1);
	}
	else if(pid == 0)
	{
		printf("pid: %d, ppid: %d\n", getpid(), getppid());
		int i = 3, j =  0;
		int k = i / j;
		printf("k: %d\n", k);
	} 
	wait(&status);
	out_status(status);
	printf("-------------------------\n");
	
	if((pid = fork()) < 0)
	{
		perror("fork error");
		exit(1);
	}
	else if(pid == 0)
	{
		printf("pid: %d, ppid: %d\n", getpid(), getppid());
		pause();// 暂停,等待一个信号来唤醒/终止
	/*	int i = 0;
		while(++i > 0)
			sleep(3);
	*/
	}
	//wait(&status);
	
	// 用waitpid的非阻塞方式
	do
	{
		pid = waitpid(pid, &status, WNOHANG | WUNTRACED);
		if(pid == 0)
			sleep(1);
	}while(pid == 0);
 
	out_status(status);
	
	return 0;
}

程序中第一个子进程算是正常退出,所以最后返回其退出状态码为3;
程序中第二个子进程是除0操作,属于异常终止;
程序中第三个子进程如果使用pause或while循环会让程序“暂停”下来,但是这里的暂停并不是WIFSTOPPED/WSTOPSIG(status)所说的暂停。这里程序其实并没有停下来,只是不停的在做循环,我们可以通过kill将其杀掉,同样是属于异常终止;
那么要想实现上述所说的第三种状态,即子进程在退出前曾暂停过,我们就不能使用wait函数来回收子进程,而是要使用waitpid并且配合WNOHANG和WUNTRACED选项。

下图是程序执行后的运行结果:

> ./test_process 
pid: 72905, ppid: 72904
normal exit: 3
--------------------------
pid: 72906, ppid: 72904
abnormal term: 8
-------------------------
pid: 72911, ppid: 72904


可见,程序执行到pause出停了下来,相当于是在做死循环;

下面给该进程一个暂停信号,kill -SIGSTOP 72904 ,执行结果如下:

> ./test_process 
pid: 72905, ppid: 72904
normal exit: 3
--------------------------
pid: 72906, ppid: 72904
abnormal term: 8
-------------------------
pid: 72911, ppid: 72904
fish: Job 1, './test_process' has stopped

(5) 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD,SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号。

进程间通信

管道

匿名管道

匿名管道是Linux中最简单的一种进程间(半双工)通信的方式。

#include <unistd.h>
int pipe(int pipefd[2]);

pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe.
例子:

void pipe_demo()
{
    int result = -1;
    int fd[2];
    pid_t pid;

    result = pipe(fd);
    if (result == -1) {
        printf("create pipe failed\n");
        return;
    }

    pid = fork();
    if (pid < 0) {
        printf("create process failed\n");
        return;
    } else if (pid > 0) {
        printf("main process: %d\n", getpid());
        printf("%d start write\n", getpid());
        char write_str[] = "hello from main";
        write(fd[1], write_str, sizeof(write_str));

    } else {
        printf("child process: %d\n", getpid());
        printf("%d start read\n", getpid());
        char read_str[32] = "";
        read(fd[0], read_str, sizeof(read_str) - 1);
        printf("%s\n", read_str);
    }
    wait(NULL);
}

需要注意的是,fd[0]和fd[1]本质上是一个通道的两头,如果在进程内先写fd[1]后立马读fd[0],那么你大概率读到的是自己写的东西,你需要等子进程往fd[1]中写了东西再读(但一般不会这样使用,每对fd都是半双工单向通信的,你想要全双工,那么只能新建一对fd了)。

命名管道FIFO

与匿名管道不同,命名管道:

  1. 在文件系统中命名管道是以设备特殊文件的形式存在的
  2. 不同的进程可以通过命名管道共享数据

命名管道是需要程序自己创建的:

  • 通过mkfifo命令直接创建
    • mkfifo /tmp/myfifo
  • 通过mkfifo的接口创建
    • int mkfifo(const char *pathname, mode_t mode);

对FIFO来说,由于是一个具名的设备文件,那么对它的IO操作就是通过open获取的fd来进行的。
例子:

void fifo_demo()
{
    int result = -1;
    pid_t pid;

    result = mkfifo(myfifo, S_IFIFO | S_IRWXU);
    if (result == -1) {
        if (EEXIST != errno) {
            printf("create fifo file failed: %s\n", strerror(errno));
            return;
        }
    }

    pid = fork();
    if (pid < 0) {
        printf("create process failed\n");
        return;
    } else if (pid > 0) {
        int fd = open(myfifo, O_RDWR);
        if (fd < 0) {
            printf("open fifo file error\n");
            wait(NULL);
            return;
        }
        printf("main process: %d\n", getpid());
        printf("%d start write\n", getpid());
        char write_str[] = "hello from main";
        write(fd, write_str, sizeof(write_str));

    } else {
        int fd = open(myfifo, O_RDWR);
        if (fd < 0) {
            printf("open fifo file error\n");
            wait(NULL);
            return;
        }
        printf("child process: %d\n", getpid());
        printf("%d start read\n", getpid());
        char read_str[32] = "";
        read(fd, read_str, sizeof(read_str) - 1);
        printf("%s\n", read_str);
    }
    wait(NULL);
}

需要注意的是,调用open()打开命名管道的进程可能会被阻塞。但如果同时用读写方式( O_RDWR)打开,则一定不会导致阻塞;如果以只读方式( O_RDONLY)打开,则调用open()函数的进程将会被阻塞直到有写方打开管道;同样以写方式( O_WRONLY)打开也会阻塞直到有读方式打开管道。

消息队列

消息队列是内核地址空间中的内部链表,通过Linux内核在各个进程之间传递内容。消息顺序地发送到消息队列中,每个消息队列可以用IPC标识符唯一地进行标识。内核中的消息队列是通过IPC的标识符来区别的,不同的消息队列之间是相对独立的。每个消息队列中的消息,又构成一个独立的链表。
接口存在system V IPC和POSIX IPC两种接口。

system V IPC

POSIX IPC

内核中的消息队列关系

信号量

信号量是一种计数器,用来控制对多个进程共享的资源所进行的访问。他们通常被用作一个锁机制,在某个进程正在对特定资源进行操作时,信号量可以防止另一个进程去访问它。
存在system V IPC和POSIX IPC两种接口。

system V IPC

POSIX IPC

内核中的信号量关系

共享内存

共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,它是在多个进程之间对内存段进行映射的方式实现内存共享的。这是IPC最快捷的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则需要将数据通过中间机制进行转换;与此相反,共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅是地址不同而已,因此不需要进行复制,可以直接使用此段空间。
存在system V IPC和POSIX IPC两种接口。

system V IPC

POSIX IPC

内核中的共享内存关系

信号

信号(signal)机制是Unix/Linux系统中最早的进程间的通信机制。它用于在一个或多个进程之间传递异步信号。
修改信号处理:

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

int sigaction(int signum,
                     const struct sigaction *_Nullable restrict act,
                     struct sigaction *_Nullable restrict oldact);

发送信号:

#include <signal.h>
int kill(pid_t pid, int sig);

int raise(int sig);

kill向进程号为pid的进程发送信号,信号值为sig。当pid为0时,向当前系统内的所有进程发送信号。
raise在当前进程中自举一个信号sig,即向当前进程发送信号。

套接字

网络套接字

一种网络IPC

本地套接字/UNIX域套接字

本地套接字有两种类型:字节流套芥子和数据报套接字,字节流套接字类似于TCP,数据报套接字类似于UDP。
套接字地址类型:

#include <sys/un.h>
struct sockaddr_un {
      sa_family_t     sun_family;     /* Address family */
      char            sun_path[];     /* Socket pathname */
};

本地套接字的创建:

void domain_sock_demo()
{
    int error;
    int sock_unix;
    struct sockaddr_un addr_unix;
    int len_unix;

    sock_unix = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_unix < 0) {
        printf("create unix socket error: %s\n", strerror(errno));
        return;
    }
    // 删除残留路径
    unlink(mysock);

    memset(&addr_unix, 0, sizeof(addr_unix));
    addr_unix.sun_family = AF_LOCAL;
    strncpy(addr_unix.sun_path, mysock, strlen(mysock));
    len_unix = SUN_LEN(&addr_unix); 
    addr_unix.sun_path[0] = 0; // 使用格式化抽象本地地址

    error = bind(sock_unix, (struct sockaddr *)&addr_unix,
                 sizeof(struct sockaddr_un));
    if (error < 0) {
        printf("bind unix sock error: %s\n", strerror(errno));
        return;
    }

    while (1) { ; }

    close(sock_unix);
    unlink(mysock);

    return;
}

其中,在填充本地套接字名称时,使用了格式化抽象本地地址的技术,使得路径名的第一个字节为一个空字节。在路径名中空字节之后的字节才会成为抽象名字的一部分:

unix  2      [ ]         STREAM                   84015    12681/./c_snippet    @tmp/mysock@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

如果不使用格式化抽象本地地址,那么就要保证指定的本地套接字路径上的所有文件夹是存在的,否则创建socket会失败:No such file or directory
在计算实际sockaddr_un大小时,可以使用SUN_LEN宏来计算:

/* Evaluate to actual length of the `sockaddr_un' structure.  */
# define SUN_LEN(ptr) (offsetof (struct sockaddr_un, sun_path)		      \
		      + strlen ((ptr)->sun_path))

两个进程通过本地套接字通信:

void domain_sock_demo()
{
    int pid;
    pid = fork();
    if (pid < 0) {
        printf("fork error: %s", strerror(errno));
        return;
    } else if (pid > 0) {
        printf("main process: %d\n", getpid());
        domain_sock_server_demo();
    } else if (pid == 0) {
        printf("child process: %d\n", getpid());
        domain_sock_client_demo();
    }
    wait(NULL);
}

void domain_sock_server_demo()
{
    int error;
    int sock_unix;
    struct sockaddr_un addr_unix;
    int len_unix;

    sock_unix = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_unix < 0) {
        printf("create unix socket error: %s\n", strerror(errno));
        return;
    }
    // 删除残留路径
    unlink(mysock_serv);

    memset(&addr_unix, 0, sizeof(addr_unix));
    addr_unix.sun_family = AF_LOCAL;
    strncpy(addr_unix.sun_path, mysock_serv, strlen(mysock_serv));
    len_unix = SUN_LEN(&addr_unix);
    // addr_unix.sun_path[0] = 0; // 使用格式化抽象本地地址

    error = bind(sock_unix, (struct sockaddr *)&addr_unix, len_unix);
    if (error < 0) {
        printf("bind unix sock error: %s\n", strerror(errno));
        return;
    }

    if (listen(sock_unix, 128) < 0) {
        printf("listen on unix socket error: %s\n", strerror(errno));
        close(sock_unix);
        unlink(mysock_serv);
        return;
    }

    struct sockaddr_un addr_peer_unix;
    socklen_t len_peer = sizeof(addr_peer_unix);
    int connfd =
        accept(sock_unix, (struct sockaddr *)&addr_peer_unix, &len_peer);
    if (connfd < 0) {
        printf("accept on unix sock error: %s\n", strerror(errno));
        close(sock_unix);
        unlink(mysock_serv);
        return;
    }

    len_peer -= offsetof(struct sockaddr_un, sun_family);
    char *name = (char *)malloc(len_peer + 1);
    strncpy(name, addr_peer_unix.sun_path, len_peer);
    name[len_peer] = 0;
    struct stat stat_buf;
    if (stat(name, &stat_buf) < 0) {
        printf("%s not stat: %s\n", name, strerror(errno));
        free(name);
        close(sock_unix);
        unlink(mysock_serv);
        return;
    }

    // read
    char buff[128] = "";
    int readret = read(connfd, buff, sizeof(buff));
    if (readret < 0) {
        printf("read error: %s\n", strerror(errno));
    } else if (readret == 0) {
        printf("peer closed\n");
    } else {
        printf("peer send: %s\n", buff);
    }

    sleep(2);
    close(sock_unix);
    unlink(mysock_serv);

    return;
}

void domain_sock_client_demo()
{
    int ret;
    int sock_unix;
    struct sockaddr_un addr_unix, addr_unix_serv;
    int len_unix, len_unix_serv;

    sock_unix = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_unix < 0) {
        printf("create unix socket error: %s\n", strerror(errno));
        return;
    }
    // 删除残留路径
    unlink(mysock_peer);

    memset(&addr_unix, 0, sizeof(addr_unix));
    addr_unix.sun_family = AF_LOCAL;
    strncpy(addr_unix.sun_path, mysock_peer, strlen(mysock_peer));
    len_unix = SUN_LEN(&addr_unix);
    // addr_unix.sun_path[0] = 0; // 使用格式化抽象本地地址

    ret = bind(sock_unix, (struct sockaddr *)&addr_unix, len_unix);
    if (ret < 0) {
        printf("bind unix sock error: %s\n", strerror(errno));
        close(sock_unix);
        return;
    }

    addr_unix_serv.sun_family = AF_LOCAL;
    strncpy(addr_unix_serv.sun_path, mysock_serv, strlen(mysock_serv));
    len_unix_serv = SUN_LEN(&addr_unix_serv);
    if (connect(sock_unix, (struct sockaddr *)&addr_unix_serv, len_unix_serv)
        < 0) {
        printf("connect to sock server failed: %s\n", strerror(errno));
        close(sock_unix);
        return;
    }

    int write_len =
        write(sock_unix, "hello from peer", sizeof("hello from peer"));
    if (write_len < 0) {
        printf("write to sock error: %s\n", strerror(errno));
    } else if (write_len == 0) {
        printf("serv closed\n");
    } else {
        printf("write count: %d\n", write_len);
    }

    sleep(2);
    close(sock_unix);
    unlink(mysock_peer);

    return;
}

匿名套接字

#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
  • domain:表示协议族,只能为AF_LOCAL或者AF_UNIX;
  • type:表示类型,只能为0;
  • protocol:表示协议,可以是SOCK_STREAM或者SOCK_DGRAM;用SOCK_STREAM建立的套接字对是管道流,与一般的管道相区别的是,套接字对建立的通道是双向的,即每一端都可以进行读写。
  • sv:用于保存建立的套接字对

socketpair函数建立了一对匿名的已经连接的套接字,其特性由协议domain、类型type、协议protocol决定,建立的两个套接字描述符会放在sv[0]和sv[1]中。

一般使用流程:使用socketpair建立套接字描述符后,在一个进程中关闭其中的一个,在另一个进程中关闭另一个。
例子:

void domain_socketpair_demo()
{
    int sockfd[2];
    int ret;
    pid_t pid;
    ret = socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd);
    if (ret != 0) {
        printf("make socket pair failed: %s\n", strerror(errno));
        return;
    }
    pid = fork();
    if (pid < 0) {
        printf("fork error: %s", strerror(errno));
        return;
    } else if (pid > 0) {
        printf("main process: %d\n", getpid());
        close(sockfd[1]);
        char str[] = "hello from main";
        write(sockfd[0], str, sizeof(str));
        sleep(1);
        close(sockfd[0]);
    } else if (pid == 0) {
        printf("child process: %d\n", getpid());
        close(sockfd[0]);
        char str[128] = "";
        int read_chars = read(sockfd[1], str, sizeof(str));
        if (read_chars < 0) {
            printf("read error: %s\n", strerror(errno));
        } else if (read_chars == 0) {
            printf("peer socket closed\n");
        } else {
            printf("read %d from peer: %s\n", read_chars, str);
        }
        close(sockfd[1]);
        return;
    }
    wait(NULL);
    return;
}

传递文件描述符

Linux支持从一个进程中将一个已经打开的文件描述符传递给其他的进程,基本流程:

  • 创建一个字节流或者数据报的UNIX域套接字
    • 如果目标是fork一个子进程,让子进程打开描述符并将它返回给父进程,那么父进程可以用socketpair创建一个流管道,用它来传递描述字
    • 如果进程之间没有亲缘关系,那么服务器必须创建一个UNIX域字节流套接字,绑定一个路径名,让客户连接到这个套接字。然后客户端可以向服务器发送一个请求打开某个描述字,服务器将描述符通过UNIX域套接字传回。在客户端和服务器之间也可以用UNIX 数据报套接字,但不建议这样使用
  • 进程可以用任何返回描述符的UNIX函数打开,可以在进程间传递任何类型的描述符。
  • 发送进程建立一个msghdr结构,其中包含要传递的描述符。发送进程调用sendmsg通过第一步得到的UNIX域套接字发出描述符。这个时候描述符是在飞行中的,即在发送进程调用sendmsg后,在接收进程调用recvmsg之前将描述符关闭,它仍会为接收进程保持打开状态,描述符的发送导致它的访问统计数加1
  • 接收进程调用recvmsg在UNIX域套接字上接收套接字。通常接收进程收到的描述符的编号和发送进程中的描述符的编号不同。但这没问题,传递描述符不是传递描述符的编号,而是在接收进程中建立一个新的描述符,指向内核的文件表中与发送进程发送的描述符相同的项。
    例子:
/**
 * @brief send a fd
 * @param  sock_unix    domain socket
 * @param  data msg data
 * @param  bytes msg data bytes
 * @param  send_fd  fd to send by domain socket
 */
void send_fd(int sock_unix, void *data, int bytes, int send_fd)
{
    struct msghdr msghdr_send;
    struct iovec iov[1];
    char control[CMSG_SPACE(sizeof(int))];

    struct cmsghdr *pcmsghdr = NULL;
    msghdr_send.msg_control = control;
    msghdr_send.msg_controllen = sizeof(control);

    pcmsghdr = CMSG_FIRSTHDR(&msghdr_send);
    pcmsghdr->cmsg_len = CMSG_LEN(sizeof(int));
    pcmsghdr->cmsg_level = SOL_SOCKET;
    pcmsghdr->cmsg_type = SCM_RIGHTS;
    *((int *)CMSG_DATA(pcmsghdr)) = send_fd;

    msghdr_send.msg_name = NULL;
    msghdr_send.msg_namelen = 0;

    iov[0].iov_base = data;
    iov[0].iov_len = bytes;
    msghdr_send.msg_iov = iov;
    msghdr_send.msg_iovlen = 1;

    if (sendmsg(sock_unix, &msghdr_send, 0) < 0) {
        printf("sendmsg failed: %s\n", strerror(errno));
    }
}

/**
 * @brief recv a fd
 * @param  sock_unix    connected domain socket
 * @param  data msg data
 * @param  bytes    msg bytes
 * @param  recv_fd  fd to recv by domain socket
 * @return int recv msg len
 */
int recv_fd(int sock_unix, void *data, int bytes, int *recv_fd)
{
    struct msghdr msghdr_recv;
    struct iovec iov[1];
    ssize_t n;

    char control[CMSG_SPACE(sizeof(int))];

    struct cmsghdr *pcmsghdr;
    msghdr_recv.msg_control = control;
    msghdr_recv.msg_controllen = sizeof(control);

    msghdr_recv.msg_name = NULL;
    msghdr_recv.msg_namelen = 0;

    iov[0].iov_base = data;
    iov[0].iov_len = bytes;
    msghdr_recv.msg_iov = iov;
    msghdr_recv.msg_iovlen = 1;
    if ((n = recvmsg(sock_unix, &msghdr_recv, 0)) <= 0) {
        printf("recvmsg nothing: %s\n", strerror(errno));
        return n;
    }

    pcmsghdr = CMSG_FIRSTHDR(&msghdr_recv);
    if (pcmsghdr != NULL && pcmsghdr->cmsg_len == CMSG_LEN(sizeof(int))) {
        if (pcmsghdr->cmsg_level != SOL_SOCKET) {
            printf("control level is not socket!\n");
        }
        if (pcmsghdr->cmsg_type != SCM_RIGHTS) {
            printf("control type is not SCM_RIGHTS!\n");
        }
        *recv_fd = *(int *)CMSG_DATA(pcmsghdr);
    } else {
        *recv_fd = -1;
    }
    return n;
}

server端:

#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int sock_unix = create_domain_sock(mysock_serv);
    if (sock_unix < 0) {
        printf("create domain sock failed!\n");
        return -1;
    }
    printf("create and bind unix socket down\n");

    if (listen(sock_unix, 128) < 0) {
        printf("listen on unix socket error: %s\n", strerror(errno));
        close(sock_unix);
        unlink(mysock_serv);
        return -1;
    }

    int connfd = accept_domain_sock(sock_unix);
    if (connfd < 0) {
        printf("accept sock failed!\n");
        close(sock_unix);
        unlink(mysock_serv);
        return -1;
    }
    printf("accept an unix socket down\n");

    char c;
    int fd;
    recv_fd(connfd, &c, 1, &fd);
    printf("recvmsg from unix socket down\n");
    if (fd > 0) {
        write(fd, "hello from server", sizeof("hello from server"));
        close(fd);
    }

    sleep(1);

    close(sock_unix);
    unlink(mysock_serv);

    return 0;
}

client端:

#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int fd = open(myfile, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);
    if (fd < 0) {
        printf("open file failed: %s\n", strerror(errno));
        return -1;
    }

    int sock_unix = create_domain_sock(mysock_peer);
    if (sock_unix < 0) {
        printf("create domain sock failed!\n");
        return -1;
    }
    printf("create and bind unix socket down\n");

    int ret = connect_domain_sock(sock_unix, mysock_serv);
    if (ret < 0) {
        printf("connect sock failed!\n");
        close(sock_unix);
        unlink(mysock_peer);
        return -1;
    }
    printf("connect to unix socket down\n");

    send_fd(sock_unix, "", 1, fd);
    printf("sendmsg down\n");

    sleep(1);

    close(fd);
    close(sock_unix);
    unlink(mysock_peer);

    return 0;
}

参考

【Linux】消息传递的艺术:探索Linux消息队列机制
Linux 进程间通信之消息队列:原理 + API 与实战 (System-V IPC)

posted @ 2026-03-01 20:20  main_c  阅读(1)  评论(0)    收藏  举报  来源