Redis Source Code Read Log (2. 父子进程继承了什么 参考《Unix环境高级编程第三版》)

Part 1: fork 介绍

#include <unistd.h>

pid_t fork(void);

由 fork 创建的子进程,该函数被调用一次,但是会返回两个返回值。

第一个返回值:子进程的pid。返回给父进程,即当前调用fork的进程。

第二个返回值:0,返回给子进程。即新产生的进程。

为什么需要将子进程的pid返回给父进程?

因为:一个父进程可能有很多子进程。也有可能没有。Linux系统没有提供API,使得父进程去获取子进程的pid。那么此时fork返回子进程pid便于父进程进行子进程管理。

为什么子进程中返回0,而不是,比如返回父进程的pid?

因为:一个子进程有且仅有一个父进程。而子进程中可以通过 getppid 获取到父进程的pid。所以,没有必要返回比如父进程pid给到子进程,只需返回特定标志值,0,区分出这是在子进程中即可。

简单示例:

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

#include <unistd.h>

int g_var = 10;

void task()
{
    printf("PID=%d, %s, var=%d, &var=0x%p\n",
        getpid(),__func__, g_var, &g_var);
}

void parent_task()
{
    task();    
}

void child_task()
{
    g_var += 10;
    task();
}

int main()
{
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork");
        printf("error\n");
        return 0;
    }
    if (pid == 0)
    {
        child_task();
    }
    else if (pid > 0)
    {
        printf("child pid: %d\n", pid);
        sleep(1);
        parent_task();
    }

    sleep(1);
    return 0;
}

运行结果:

在 fork 返回 pid 为 0 的 case中,其实执行的是子进程的task。在pid > 0 的case中,执行的是父进程的任务。

在子进程的task中,我们改变了全局变量的值。但是,在父进程的task中,全局变量的值并未改变。

另外,两个进程中的全局变量的地址是一样的。

Part 2 继承与共享

fork出来的子进程是父进程的“副本”。子进程获取到了父进程的数据空间,堆、栈的“副本”。该“副本”是子进程所拥有的副本。父子进程并不共享这些存储空间部分父子进程共享的是程序的Text区

I/O数据共享

ISO C 标准I/O的三种缓冲机制

1. 全缓冲

2. 行缓冲

3. 不带缓冲

ISO C 当且仅当标准输入、输出并不指向交互设备,比如控制台,才是全缓冲的。标准错误不允许全缓冲。

一般系统采用如下方案:

标准错误,不带缓冲。指向终端设备,如控制台,采用行缓冲,否则,采用全缓冲。默认全缓冲大小是1个内存页,即4kB.

我们可以通过以下两个接口进行标准I/O的缓冲类型。

#include <stdio.h>

void setbuf(FILE* restrict fp, char*  restrict buf);

int setvbuf(FILE* restirct fp, char* restrict buf, int mode, size_t size);

示例:

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

#include <unistd.h>

int g_var = 10;

FILE* fp = NULL;

void task()
{
    printf("PID=%d, %s, var=%d, &var=0x%p\n",
        getpid(),__func__, g_var, &g_var);
    if (fp) fclose(fp), fp = NULL;
}

void parent_task()
{
    task();    
}

void child_task()
{
    g_var += 10;
    fprintf(fp, "PID=%d, %s, var=%d, &var=0x%p\n",
        getpid(),__func__, g_var, &g_var);
    task();
}

int main()
{
    fp = fopen("std_file.log","wb");
    fprintf(fp, "%s, var=%d, &var=0x%p\n",
        __func__, g_var, &g_var);
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork");
        printf("error\n");
        return 0;
    }
    if (pid == 0)
    {
        child_task();
    }
    else if (pid > 0)
    {
        printf("child pid: %d\n", pid);
        sleep(1);
        parent_task();
    }

    sleep(1);
    return 0;
}

第一 fp被继承下来,可以正常操作。

第二 main函数中的fprintf 内容被输出了两次.其实并不是被调用了两次,而是由于I/O 缓冲区,fork之后,子进程也将对应缓冲区的内容复制到了子进程。I/O 缓冲区中的数据也是会被继承的。查看FILE 源码即可知道。标准库,就是针对每一个FILE* 申请了一个默认(BUFSIZ)大小的BUF,文件的操作,就是写入到这段内存中。这个内存数据亦复制到子进程中。还有就是,文件fp被复制了,输出文件也没有被冲乱。所以不仅是被简单继承,而且内部的一些状态,是被共享了的。后面会讨论这种文件共享。

针对 fd,不存在I/O缓冲区,只有内核缓冲区。那么内核缓冲区,是否会被复制呢?

代码略,采用 open write 系统调用,结果如下:

这样一来,就不会有两行 main, var ....

文件共享

fork的子进程也会复制父进程所有的打开的文件描述符,所谓复制,实际的效果类似执行 dup 函数。父子进程针对每一个打开的描述符,共享一个文件表项目。

Linux I/O 的数据结构:

Linux 多个独立进程打开同一文件:

dup (duplicate)复制 fd 的方式:

close-on-exec fd flag,我们暂且不用考虑,毕竟redis中仅仅是fork,而并未采用 exec 函数族,后续讨论fork与exec的使用。

对比上面的图,可以看出独立进程open同一个文件,dup对fd的复制。fork对fd的进程采用的就是“文件共享”式类似dup方式的复制继承。并且,父子进程都需要考虑都fd的自行管理。否则,对于服务程序而言,可能会造成handle 泄露

socket 也是一种fd。如此而已。

验证:

tcp_server, on Linux ubuntu 18.04LTS

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>

#define PORT    8888
#define BKLOG   5

void child_task(int fd)
{
    char bf[1024];
    int n = 0;
    while (1)
    {
        n = read(fd, bf, 1023);
        if (n < 0)
        {
            perror("read");
            printf("%d, read error\n", getpid());
            return ;
        }
        if (n == 0)
        {
            printf("%d, client closed\n", getpid());
            return ;
        }
        bf[n] = 0;
        printf("%d: process clients message: %s\n", getpid(), bf);
        n = write(fd, "Hello", 5);
    }
}

int main()
{
    int reuse = 1, ret = -1, sock = -1, cfd = -1;
    struct sockaddr_in saddr = {};
    struct sockaddr_in caddr = {};
    socklen_t len = 0;
    
    sock = socket(AF_INET, SOCK_STREAM, 0);

    ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    if (ret == -1) perror("setsockopt"), exit(0);

    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(PORT);
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);

    ret = bind(sock, (void*)&saddr, sizeof(saddr));
    if (ret == -1) perror("bind"), exit(0);

    ret = listen(sock, BKLOG);
    if (ret == -1) perror("listen"), exit(0);

    char str[512] = {};

    while (1)
    {
        cfd = accept(sock, (void*)&caddr, &len);
        pid_t pid = fork();
        if (pid < 0)
        {
            perror("fork");
            return 0;
        }
        if (pid == 0)
        {
            child_task(cfd);
            close(cfd);
            break;
        }
        printf("new connection [%s:%d], server close cfd[%d]\n",  inet_ntop(AF_INET, &caddr.sin_addr, str, sizeof(str)),
            ntohs(caddr.sin_port), cfd);
        // server main process, must close this cfd, otherwise handle-leak
        close(cfd);
    }

    return 0;
}

client: Windows, with 5 thread, and file client connecting at the same time:

#include <iostream>
#include <string>
#include <vector>
#include <thread>

#include <cstdio>
#include <cstdlib>

#include <winsock2.h>
#include <windows.h>

#pragma comment(lib,"ws2_32.lib")

#define PORT 8888

static const std::vector<std::string> CMDS = {"Hello", "World", "One", "World", "One", "Dream"};

int task();

int main()
{
	std::thread t1(task);
	std::thread t2(task);
	std::thread t3(task);
	std::thread t4(task);
	std::thread t5(task);

	t1.join();
	t2.join();
	t3.join();
	t4.join();
	t5.join();
	return 0;
}

int task()
{
	WORD sockVersion = MAKEWORD(2, 2);
	WSADATA data;
	if (WSAStartup(sockVersion, &data) != 0)
	{
		return 0;
	}

	SOCKET sclient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sclient == INVALID_SOCKET)
	{
		printf("invalid socket !");
		return 0;
	}

	struct sockaddr_in serAddr;
	serAddr.sin_family = AF_INET;
	serAddr.sin_port = htons(PORT);
	serAddr.sin_addr.S_un.S_addr = inet_addr("192.168.0.101");
	if (connect(sclient, (struct sockaddr *)&serAddr, sizeof(serAddr)) == SOCKET_ERROR)
	{
		printf("connect error with code: %d\n", GetLastError());
		closesocket(sclient);
		return 0;
	}
	std::string cmd = CMD_PING;
	bool flag = false;
	for (int i = 0; i < CMDS.size(); i++)
	{
		cmd = CMDS[i];
		send(sclient, cmd.c_str(), cmd.size(), 0);
		char recData[255];
		int ret = recv(sclient, recData, 254, 0);
		if (ret > 0)
		{

			recData[ret] = 0x00;
			std::string reply = recData;
			std::vector<std::string> v;
			printf("-------- %d ---------\n", i);
			for (int i = 0; i < ret; i++)
			{
				char c = recData[i];
				if (c == '\r')
				{
					c = '_';
				}
				else if (c == '\n')
				{
					c = '|';
				}
				printf("%c", c);
			}
			printf("\n");
			//if (StringSplit(v, reply, '\n'))
			{
				//printf("%s\n", recData);
			}
		}
		else if (ret == 0)
		{
			printf("Server closed socket!\n");
		}
		Sleep(1000 * 3);
	}
	closesocket(sclient);
	WSACleanup();
	return 0;
}

Redis RDB 启动进程的方式,就是该方式。继承对应的内存,共享fd等等。

Redis的 还会 通过 管道,与子进程通讯,获取到子进程处理完比如save请求响应结果。

 

PS: 附一段简单代码,可以说明 ISO C 的标准I/O 与 Linux 的I/O 行为区别,以及在 ISO C 的 exit() 与 Linux _exit() 的不同行为。标准I/O的缓冲清理工作,在exit 时会被正常处理,但是 _exit() 时不会被清理。再者,就是标准I/O与系统调用的区别。一般程序可能不会考虑这种,但是涉及的父子进程编程的场景,这种情况需要格外小心。还有就是,便于我们更好的去理解ISO C在stdlib.h 中提供的 int atexit(void (*func)(void)); 函数的意义了。另外,printf 中需要注意有没有 \n 换行符。我们向控制台设备输出内容,ISO C 采用的是行缓冲策略,如果加了 \n 或者 flush,那么就达不到预期效果了。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值