linux系统编程(五)

1、信号

信号是事件发生时对进程的通知机制,针对每个信号都定义了一个唯一的整数,这些整数定义在signal.h中。

常见信号如下:

  • SIGABRT:进程调用abort函数,系统向进程发送此信号,终止进程并产生核心转储文件。
  • SIGBUS:表示出现了某种内存访问错误;
  • SIGCHLD:父进程的某一子进程终止;
  • SIGINT:用户输入终端终端字符(ctrl+c)
  • SIGKILL:必杀信号,程序无法阻塞、忽略或者捕获
  • SIGPIPE:向管道、FIFO或者socket写入信息时,没有相应的阅读进程;
  • SIGQUIT:键盘输入退出字符(ctrl+\)
  • SIGSEGV:程序对内存的引用无效时会产生此信号。

signal系统调用可以用来改变信号处置:

#include <signal.h>
void (*signal(int sig, void (*handler)(int)))(int);

第一个参数表示需要修改的信号,第二个参数handler是修改后的处置函数,返回值是之前的信号处置函数。

我们可以使用kill来发送信号

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

参数pid用于标识一个或者多个目标进程:

  • pid大于0,发送信号给指定进程;
  • pid等于0,发送信号给与调用进程同组的所有进程,包括调用进程自身;
  • pid小于-1,向组ID等于该pid绝对值的进程组内下属进程发送信号;
  • pid等于-1,调用进程有权将信号发往的每一个目标进程,出去init和调用进程自身。特权进程发起这一调用,会发送信号给所有进程,这也被称为广播信号;

如果无进程与指定pid匹配,kill调用失败,errno设置为ESRCH。

除了可以使用kill发送信号,我们还可以使用raise发送信号:

#include <signal.h>
int raise(int sig);

raise是对自身发送信号,相当于调用kill(getpid(), sig)。

以下是示例代码:

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

extern const char *const sys_siglist[];

int sigIntCnt = 0;

static void sigHandler(int sig) {
        switch(sig) {
                case SIGINT:
                        printf("current sigIntCnt:%d\n", sigIntCnt++);
                        if(sigIntCnt == 3) {
                                exit(1);
                        }
                        break;
                case SIGQUIT:
                        printf("recevie SIGQUIT, exit!\n");
                        exit(1);
                        break;
                default:
                        printf("receive msg:%s\n", strsignal(sig));
                        break;
        }
}

int main(int argc, char **argv) {

    signal(SIGINT, sigHandler);
    signal(SIGQUIT, sigHandler);

    int n = 0;
    while(1) {
            printf("n:%d\n", n++);
            sleep(1);
    }

    return 0;
}

多个信号可以使用一个称之为信号集的数据结构来标识,该数据类型为sigset_t。下面是一组操作信号集的函数:

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);

创建sigset_t变量后,必须要用上面两个函数来初始化信号集,不能使用memset来初始化。

信号集初始化完成后,可以向信号集中添加或删除单个信号:

#include <signal.h>
int sigaddset(sigset_t *set, int sig);
int sigdelset(sigset_t *set, int sig);

可以用sigismember判断信号集中是否包含某个信号:

#include <signal.h>
int sigismember(const sigset_t *set, int sig);

内核会为每一个进程维护一个掩码(一组信号),阻塞其针对该进程的传递。信号掩码属于线程属性,多线程中每个线程都能使用pthread_sigmask来检查或修改信号掩码。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

使用sigprocmask可以修改信号掩码,也可以获取现有掩码。根据参数how可以确定给掩码带来的变化。

  • SIG_BLOCK:将set信号集内的信号添加到信号掩码中,做并集
  • SIG_UNBLOCK:将set中的信号从当前掩码中移除
  • SIG_SETMASK:将set信号集赋给信号掩码,替换

除了可以用signal来改变信号处置外,还可以使用sigaction做信号处置。

#include <signal.h>
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
}

sa_mask定义了一组信号,在调用sa_handler所定义的处理器程序时将阻塞该信号。比如程序运行信号处理函数时,该信号再次到来,信号将不会中断自己。

pause会暂停进程的执行,知道信号处理函数中断该调用。

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

信号处理函数有一种常见设计:

  • 信号处理函数设置全局性表示变量并退出,主程序对该标志进行周期性检查,发现置位就采取相应动作。进行这种周期性检查时可以让信号处理函数向一个专用管道写入一个字节数据。

我们在设计信号处理函数时要确保处理函数本身是可重入的。信号处理函数可能会更新errno,所以一般情况下进入信号处理函数时记录errno,退出时恢复errno。

如果想要让主程序和信号处理函数共享全局变量,可以进行如下声明:

volatile sig_atomic_t flag;

以下是一个简单的示例:

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

static void sigHandler(int sig) {
	printf("enter sigHandler, sig:%d\n", sig);
	int cnt = 3;
	switch(sig) {
		case SIGINT:
			while(cnt--) {
				printf("process SIGINT, current sleep cnt:%d\n", cnt);
				sleep(1);
			}
			break;
		case SIGQUIT:
			printf("process SIGQUIT\n");
			exit(1);
			break;
		default:
			break;
	}
}

int main(int argc, char **argv) {
	struct sigaction sigact;
	sigemptyset(&sigact.sa_mask);
	sigaddset(&sigact.sa_mask, SIGINT);
	sigaddset(&sigact.sa_mask, SIGQUIT);
	sigact.sa_handler = sigHandler;

	sigaction(SIGINT, &sigact, NULL);

	signal(SIGQUIT, sigHandler);

	while(1) {
		sleep(1);
	}

	return 0;
}

2、进程

我们可以使用系统调用fork创建一个新的进程,子进程创建时会拷贝父进程的文本段、数据段、堆、栈,但是后续可以各自修改栈、堆中的数据,不影响另一个进程。进程创建完成后,两个进程都会从fork返回处继续执行。

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

无法创建子进程,fork返回-1。在父进程中fork返回新创建的子进程ID,在子进程中fork返回0。

fork时子进程会得到父进程文件描述符的副本,包含偏移量文件状态标志等。如果子进程更新了文件偏移量,那么也会影响到父进程中的文件描述符。

进程有两种中止方式,一种是异常(abnormal)中止,还有一种是_exit系统调用正常(normal)中止。

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

_exit的参数status定义了中止的状态,父进程可以调用wait获取该状态。一般来说状态为0表示进程功成身退,非0值表示进程异常退出。

一般来说程序会使用库函数exit:

#include <stdlib.h>
void exit(int status);

exit会调用退出处理函数、刷新stdio缓冲区、调用_exit。

上面提到了wait,系统调用wait用于等待调用进程的任一子进程中止:

#include <sys/wait.h>
pid_t wait(int *status);

status返回的是子进程的中止状态。

wait调用之后,如果之前已经有子进程中止,wait会立即返回。如果没有子进程中止,调用会一直阻塞直至某个子进程中止。wait返回值为中止子进程的ID,参数status为子进程的返回状态。

如果进程没有子进程,那么wait会返回-1,同时errno会被设置为ECHILD。

以下是一个简单的代码示例:

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

int main(int argc, char **argv) {

	int fd = open("tmp.txt", O_RDWR | O_CREAT, 0644);
	if(fd < 0) {
		printf("open file failed, err:%s\n", strerror(errno));
		exit(1);
	} else {
		printf("open file success, fd:%d\n", fd);
	}

	pid_t pid = fork();
	if(pid == 0) {
		printf("Hello, I am child process, id:%d, parent:%d\n", getpid(), getppid());
	} else if (pid > 0) {
		printf("Hello, I am parent process, id:%d, child:%d\n", getpid(), pid);
	} else {
		printf("fork fail\n");
		exit(1);
	}

	const char *str = pid==0?"child":"parent";
	
	off_t offset = lseek(fd, 0, SEEK_CUR);
	printf("%s pid:%d fd:%d, offset:%ld\n",
			str, pid, fd, offset);

	const char *txt = "today is 2024/12/21, now 21:24!";
	if(pid == 0) {
		ssize_t written = write(fd, txt, strlen(txt));
		if(written < strlen(txt)) {
			printf("%s pid:%d, write fail, written:%ld\n", str, getpid(), written);
		} else {
			printf("%s pid:%d, write OK, written:%ld\n", str, getpid(), written);
		}
		exit(0);
	}

	pid_t child = wait(NULL);
	if(child > 0) {
		printf("%s pid:%d, child exit:%d\n", str, getpid(), child);
	} else {
		printf("%s pid:%d, child exit fail:%s\n", str, getpid(), strerror(errno));
	}

	offset = lseek(fd, 0, SEEK_CUR);
	printf("%s pid:%d fd:%d, after child write offset:%ld\n",
			str, pid, fd, offset);

	close(fd);
	return 0;
}

// 测试结果
/*
open file success, fd:4
Hello, I am parent process, id:8424, child:8425
parent pid:8425 fd:4, offset:0
Hello, I am child process, id:8425, parent:8424
child pid:0 fd:4, offset:0
child pid:8425, write OK, written:31
parent pid:8424, child exit:8425
parent pid:8425 fd:4, after child write offset:31
*/

wait有很多限制:

  • 父进程创建了多个子进程,wait无法等待某个特定子进程完成,只能按顺序等待下一个子进程的中止;
  • 如果子进程退出,wait总是保持阻塞;
  • wait只能发现已经中止的进程,如果子进程因为某个信号而停止,或者收到型号恢复,wait就无能为例了。
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

参数pid表示要等待的具体子进程:

  • pid等于0,表示等待与调用进程同一进程组的所有子进程;
  • pid小于-1,等待进程标识符与pid绝对值相等的所有子进程;
  • pid等于-1,等待任意子进程,和wait等效。

option是一个掩码,它有以下几个选项:

  • WUNTRACED:除了返回中止子进程的信息外,还会返回因信号而停止的子进程信息;
  • WCONTINUED:因收到SIGCONT信号恢复执行的已经之子进程的状态信息;
  • WNOHANG:如果指定的子进程状态未发生改变,立即返回,不会阻塞。这种情况waitpid返回0。如果没有与指定参数相匹配的子进程,waitpid报错,errno设置为ECHILD。

某一子进程的父进程终止后,它的父进程会变成1(init),这是判断父进程是否存在的方法。

子进程死亡后,内核会将它转换为僵尸进程,父进程需要调用wait来释放子进程资源。如果父进程没有执行wait就退出,init进程会接管子进程并自动调用wait,将僵尸进程从系统移除。

如果父进程创建了许多子进程,但是没有执行wait,那么内核的进程表将永久为该子进程保留一条记录。如果有大量僵尸进程,并且填满了进程表,将会阻碍新进程的创建。这种情况下,只有杀死父进程,才能清理这些僵尸进程。

无论一个子进程什么时候中止,系统都会向父进程发送SIGCHLD信号,默认处理是忽略。我们可以通过安装信号处理程序来捕获它们,用wait来收拾僵尸进程。不过在之前的学习中,用sigaction时我们会设置屏蔽,处理SIGCHLD时如果有新的SIGCHLD到来,此时父进程只会处理一次,这样就可能有漏网之鱼了。所以解决方案是在信号处理函数中添加while:

while(waitpid(-1, NULL, WNOHANG) > 0) {

}

以下是一个简单的代码示例:

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

static int cnt = 0;
static int total = 0;
static int quit = 0;
static void sigHandler(int sig) {
	pid_t pid = -1;
	switch(sig) {
		case SIGCHLD:
			while((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
				printf("recyle pid:%d, cnt:%d\n", pid, cnt++);
			}
			if(quit && cnt == total) {
				printf("recyle last child process, cnt:%d, total:%d, QUIT!\n", cnt, total);
				exit(0);
			}
			break;
		case SIGINT:
		case SIGQUIT:
			if(cnt == total && cnt > 0) {
				printf("already recyle all child process! cnt:%d, total:%d\n", cnt, total);
				exit(0);
			}
			quit = 1;
			printf("pid:%d wait for child process stop, cnt:%d, total:%d\n", getpid(), cnt, total);
			break;
		default:
			break;
	}
}

static void childSigHandler(int sig) {
	printf("pid:%d receive signal:%d, ignore\n", getpid(), sig);
}

int main(int argc, char **argv) {
	
	struct sigaction act;
	sigemptyset(&act.sa_mask);
	sigaddset(&act.sa_mask, SIGCHLD);
	sigaddset(&act.sa_mask, SIGINT);
	sigaddset(&act.sa_mask, SIGQUIT);
	act.sa_handler = sigHandler;
	sigaction(SIGCHLD, &act, NULL);
	sigaction(SIGINT, &act, NULL);
	sigaction(SIGQUIT, &act, NULL);
	
	pid_t pid = -1;
	total = 5;
	for(int tmp = 0; tmp < total; tmp++) {
		pid = fork();
		if(pid == 0) {
			sigset_t sigset;
			sigemptyset(&sigset);
			sigaddset(&sigset, SIGINT);
			sigaddset(&sigset , SIGQUIT);
			sigprocmask(SIG_BLOCK, &sigset, NULL);
			sleep((tmp+1)*2);
			printf("child process:%d exit!\n", getpid());
			exit(0);
		} else {
			printf("create child %d process OK, pid:%d\n", tmp, pid);
		}
	}

	while(1) {
		sleep(1);
	}

	return 0;
}

3、程序的执行

系统调用execve可以将新程序加载到进程的内存空间,这个过程中将丢弃旧有的程序,进程的栈、数据以及堆会被新程序的相应部件所替换。

#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);

参数pathname包含准备载入当前进程空间的新程序的路径名,可以是绝对路径也可以是相对于当前工作目录的相对路径。参数argv执行了传递给新进程的命令行参数。最后一个参数envp制定了新程序的环境列表,它是一个数组,元素格式为name=value。

execve成功调用后将永不返回,无需检查它的返回值,因为永远为-1。

还有其他的一些系统调用,它们都基于execve:

#include <unistd.h>

int execle(const char *pathname, const char *arg, ...);
int execlp(const char *filename, const char *arg, ...);
int execvp(const char *filename, char *const argv[]);
int execv(const char *pathname, char *const argv[]);
int execl(const char *pathname, const char *arg, ...);

execlp和execvp(p:path)允许只提供程序的文件名,系统会在环境变量指定的目录列表中寻找相应的执行文件。

execle、execlp、execl要求开发者在调用中一字符串列表的形式来执行参数,而不是以数组来描述argv列表,首个参数对应于argv[0],必须以NULL指针来中止参数列表,实际填入时需要将NULL转换为char*。

execvp和execv允许开发者用vector(数组)定义参数列表。

execve和execle允许开发者通过envp为新程序显式指定环境变量,envp是以NULL结尾的字符串数组,这些函数以e(environment)结尾。

#include <stdlib.h>
int system(const char *command);

程序可以通过调用system函数来执行任意的shell命令。比如:system(“ls | wc”)。system的优点是简单,代价是低效率。

以下是一个简单的exec使用示例:

#include <stdio.h>

int main(int argc, char **argv) {
	printf("myprint: ");
	for(int i = 0; i < argc; i++) {
		printf("%s ", argv[i]);
	}
	printf("\n");
	return 0;
}


#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>

int main(int argc, char **argv) {

	pid_t pid = fork();

	if(pid == 0) {
		printf("child process:%d start execle\n", getpid());
		execle("./myprint", "myprint", "Hello", "World", (char *)NULL, NULL);
	} else {
		pid = wait(NULL);
		printf("parent:%d recyle child:%d OK\n", getpid(), pid);
	}

	return 0;
}

/*
child process:2920 start execle
myprint: myprint Hello World 
parent:2919 recyle child:2920 OK
*/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/943000.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C/C++ 数据结构与算法【树和二叉树】 树和二叉树,二叉树先中后序遍历详细解析【日常学习,考研必备】带图+详细代码

一、树介绍 1&#xff09;树的定义 树 (Tree) 是n(n≥0) 个结点的有限集。 若n 0&#xff0c;称为空树; 若n > 0&#xff0c;则它满足如下两个条件: &#xff08;1&#xff09;有且仅有一个特定的称为(Root)的结点; &#xff08;2&#xff09;其余结点可分为m(m≥0)个…

MVC架构模式

分析AccountTransferServlet类都负责了什么&#xff1f; 数据接收核心的业务处理数据库表中数据的crud操作负责了页面的数据展示做了很多 在不使用MVC架构模式的前提下&#xff0c;完成银行账户转账的缺点&#xff1a; 代码的复用性太差。因为没有进行职能分工&#xff0c;没有…

打破视障壁垒,百度文心快码无障碍版本助力视障IT从业者就业无“碍”

有AI无碍 钟科&#xff1a;被黑暗卡住的开发梦 提起视障群体的就业&#xff0c;绝大部分人可能只能想到盲人按摩。但你知道吗&#xff1f;视障人士也能写代码。 钟科&#xff0c;一个曾经“被黑暗困住”的人&#xff0c;他的世界&#xff0c;因为一场突如其来的疾病&#xff0c…

【RAG实战】语言模型基础

语言模型赋予了计算机理解和生成人类语言的能力。它结合了统计学原理和深度神经网络技术&#xff0c;通过对大量的样本数据进行复杂的概率分布分析来学习语言结构的内在模式和相关性。具体地&#xff0c;语言模型可根据上下文中已出现的词序列&#xff0c;使用概率推断来预测接…

48页PPT|2024智慧仓储解决方案解读

本文概述了智慧物流仓储建设方案的行业洞察、业务蓝图及建设方案。首先&#xff0c;从政策层面分析了2012年至2020年间国家发布的促进仓储业、物流业转型升级的政策&#xff0c;这些政策强调了自动化、标准化、信息化水平的提升&#xff0c;以及智能化立体仓库的建设&#xff0…

Matlab环形柱状图

数据准备&#xff1a; 名称 数值 Aa 21 Bb 23 Cc 35 Dd 47 保存为Excel文件后&#xff1a; % Load data from Excel file filename data.xlsx; % Ensure the file is in the current folder or provide full path dataTable readtable(filena…

flask后端开发(3):html模板渲染

目录 渲染模板html模板获取路由参数 gitcode地址&#xff1a; https://gitcode.com/qq_43920838/flask_project.git 渲染模板 这样就能够通过html文件来渲染前端&#xff0c;而不是通过return了 html模板获取路由参数

15 break和continue

while True: content input("请输入你要喷的内容") print("发送给下路",content) #上述的程序如果没有外力干扰&#xff1a;程序会一直进行输入下去 #break:就能让当前这个循环立即进行停止 while True: content input("请输入…

Python9-作业2

记录python学习&#xff0c;直到学会基本的爬虫&#xff0c;使用python搭建接口自动化测试就算学会了&#xff0c;在进阶webui自动化&#xff0c;app自动化 python基础8-灵活运用顺序、选择、循环结构 作业2九九乘法表三种方式打印九九乘法表使用两个嵌套循环使用列表推导式和…

微信小程序 不同角色进入不同页面、呈现不同底部导航栏

遇到这个需求之前一直使用的小程序默认底部导航栏&#xff0c;且小程序默认入口页面为pages/index/index&#xff0c;要使不同角色呈现不同底部导航栏&#xff0c;必须要在不同页面引用不同的自定义导航栏。本篇将结合分包&#xff08;subPackages&#xff09;展开以下三步叙述…

表达式语句、复合语句和空语句

欢迎拜访&#xff1a;雾里看山-CSDN博客 本篇主题&#xff1a;表达式语句、复合语句和空语句 发布时间&#xff1a;2024.12.26 隶属专栏&#xff1a;C语言 目录 1. 表达式语句定义作用常见类型赋值语句函数调用语句 2. 复合语句定义作用变量作用域 3. 空语句定义作用 1. 表达式…

Linux arm 编译安装glibc-2.29

重要的话说三遍&#xff1a; &#xff01;&#xff01;&#xff01;&#xff01;&#xff01;不要轻易自己去安装glibc&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; &#xff01;&#xff01;&#xff01;&#xff01;&#xff01;不要轻易自己去安装glibc&a…

20241225在ubuntu22.04.5下使用smartmontools命令查看ssd的寿命

20241225在ubuntu22.04.5下使用smartmontools命令查看ssd的寿命 2024/12/25 15:10 rootrootrootroot-ThinkBook-16-G5-IRH:~$ sudo apt install smartmontools rootrootrootroot-ThinkBook-16-G5-IRH:~$ sudo fdisk -l Disk /dev/nvme0n1: 3.73 TiB, 4096805658624 bytes, 800…

大数据学习之Redis 缓存数据库二,Scala分布式语言一

一.Redis 缓存数据库二 26.Redis数据安全_AOF持久化机制 27.Redis数据安全_企业中该如何选择持久化机制 28.Redis集群_主从复制概念 29.Redis集群_主从复制搭建 30.Redis集群_主从复制原理剖析 31.Redis集群_哨兵监控概述 32.Redis集群_配置哨兵监控 33.Redis集群_哨兵监控原理…

Datawhale AI 冬令营学习笔记-零编程基础制作井字棋小游戏

井字棋小游戏是通过豆包MarsCode实现的&#xff0c;没有改动任何的代码&#xff0c;全部是通过对话让AI进行优化和改进。 开始进入正题&#xff1a;进入豆包MarsCode在线IDE&#xff0c;直接点击上方蓝字&#xff0c;或复制链接打开: 豆包 MarsCode - 编程助手。 IDE界面&…

vscode+编程AI配置、使用说明

文章目录 [toc]1、概述2、github copilot2.1 配置2.2 使用文档2.3 使用说明 3、文心快码&#xff08;Baidu Comate&#xff09;3.1 配置3.2 使用文档3.3 使用说明 4、豆包&#xff08;MarsCode&#xff09;4.1 配置4.2 使用文档4.3 使用说明 5、通义灵码&#xff08;TONGYI Lin…

Redis数据结构和内部编码以及单线程架构

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 Redis数据结构和内部编码以及单线程架构 收录于专栏[redis] 本专栏旨在分享学习Redis的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 …

虚拟机Hyper-V,安装网络宝塔Docker

我下载的是centos-min大小1G&#xff0c;安装后没网络&#xff0c; 关闭防火墙&#xff0c;网络&#xff0c;修改onBootyes,这里需要看下network-Scripts下有什么文件。 然后就可以访问网络了 虚拟机的设置也是默认就好 网络需要设置允许共享-重要 urlhttps://download.bt.cn/i…

红魔电竞PadPro平板解BL+ROOT权限-KernelSU+LSPosed框架支持

红魔Padpro设备目前官方未开放解锁BL&#xff0c;也阉割了很多解锁BL指令&#xff0c;造成大家都不能自主玩机。此规则从红魔8开始&#xff0c;就一直延续下来&#xff0c;后续的机型大概率也是一样的情况。好在依旧有开发者进行适配研究&#xff0c;目前红魔PadPro平板&#x…

Linux-----进程处理(文件IO资源使用)

下面代码是通过父进程和子进程对同一个文件IO资源进行操作&#xff0c;父进程和子进程都对这个进程进行写入操作&#xff0c;我们都知道这两个进程实际上是并发的&#xff0c;所以需要一个同步机制来去操作同一个资源&#xff08;后面再深入去说明同步的api&#xff0c;这里使用…