The birth of clone of Fork trilogy

This article is the postscript of the fork trilogy, it is recommended to read it first:

In this article, after the traditional UNIX fork, I will give the wonderful clone system call, a variant of the traditional UNIX fork in the Linux kernel.


To understand the original meaning of fork, we still have to look at the original paper A Multiprocessor System Design , which Melvin Conway proposed the idea of ​​fork  :

https://archive.org/details/AMultiprocessorSystemDesignConway1963/page/n7  The concepts of "process"  and  "processpr" :

  • A process need not be specific to be processed on a processor.

  • A processor does not necessarily handle a specific process.

  • The number of processes and the number of processors in the system need not be equal.

Fork provides a means to realize the above-mentioned core ideas. Later, fork was introduced to UNIX systems and became a common operation that has remained unchanged for decades to create new processes.

What’s more interesting is that UNIX fork is well-known through the famous fork-exec sequence, not because of the parallel multiprocessing it provides. This may be because after the emergence of the thread concept, parallel processing is The thread is responsible, so no one remembers fork.

If a series of processes are  completely parallel  , then they have no resources to be interdependent. This is the basis of modern operating system process (process) abstraction. It can be seen that the modern operating system based on process abstraction is itself a parallel system. In a system that can be parallelized, resources are isolated between processes. If join operations are needed, the IPC mechanism is introduced.

Let's see how efficient the parallel multiprocessing provided by fork was before the advent of threads. The most typical example is the TCP service programming model:

void handle_request(int csd)
{
	...
	// 读取请求
	ret = recv(csd, buf_req, len);
	ret = read(fd, buf_tosend, ret);
	ret = send(csd, buf_tosend, ret);
	close(csd);
}
void server(int sd)
{
	...
	while (1) {
		csd = accept(sd, 10);
		if (fork() == 0) {
			close(sd);
			handle_request(csd); // 可并行处理
		}
	}
}

This has almost become a server programming paradigm, a prerequisite for understanding and designing select/poll/epoll programs, and a foundation for understanding later Apache Web Server and Nginx.

The above simple code, how do I implement it with the CreateProcess API of Windows?

Do not use thread API, only process API. To process multiple requests in parallel, CreateProcess needs to load a disk program image to execute handle_request. The image program may look like the following (this is not the most efficient way of writing, this is just A direct way of writing):

void handle_request(int csd)
{
	...
	// 读取请求
	ret = recv(csd, buf_req, len);
	ret = read(fd, buf_tosend, ret);
	ret = send(csd, buf_tosend, ret);
	close(csd);
}
int main(int argc, char **argv)
{
	char *client_info = argv[1];
	int sd;

	sd = GetOrCreateSocket(client_info);
	handle_request(sd);
}

We know that the image overhead of loading a program is very large, but it has to be so for parallel processing, otherwise Windows must serially process handle_reques and the following accept. Windows does not have a fork, and it does not have a mechanism that can realize the fork of the process at any point.

Of course, in reality, Windows can use the multithreading API CreateThread to do this. It can also be said that multithreading is more efficient than multiprocess solutions. But if there is no multithreading, presumably Windows can only swallow and sigh in the face of the provocation of fork.

Therefore, UNIX fork has two levels of meaning:

  1. To create a new process, the fork-exec sequence (not fork itself) competes with Windows CreateProcess or POSIX spawn.

  2. Parallel multiprocessing, fork competes for multithreading as a multi-process.

Obviously, at any level, fork has fallen behind the opponent:

  1. Create a new process, CreateProcess/spawn eliminates unnecessary resource copy operations.

  2. Parallel multi-processing, multi-threaded sharing of resources replaces expensive IPC.

As an optimization or replacement of multi-process, the essence of multi-threading and the original meaning of fork do not seem to be too different. The only difference seems to be the depth of resource sharing.

The original meaning of fork will be continued and sublimated in the design of Linux kernel task!

The designers of the Linux kernel seem to have realized this a long time ago. In the early days, the Linux kernel did not design a structure representing the process, but only designed a task_struct (hereinafter referred to as task), the structure Contains  the minimum things needed to make a stream of instructions run!  Therefore it does not contain fields specific to the process or thread concept.

What exactly is a task object or a group of tasks depends on how you deploy it!  It's like using the same words, different combinations, or curses or blessings.

A task object is just a raw material, and its resource sharing relationship with other task objects determines what it is.

It's time to release this picture:640?wx_fmt=png

enum pid_type
{
    PIDTYPE_PID,   
    PIDTYPE_TGID, 
    PIDTYPE_PGID,
    PIDTYPE_SID,
    PIDTYPE_MAX
};

For more explanation on the above figure, see the following article  Naive UNIX-Process/Thread Model:

https://blog.csdn.net/dog250/article/details/40208219

Corresponding to the flexible design of the underlying task, the application must be given its interface to adapt to this flexibility. This adaptation is the Linux clone system call, which already exists in the very early Linux kernel (at least version 2.2):

#define _GNU_SOURCE
#include <sched.h>

int clone(int (*fn)(void *), void *child_stack,
          int flags, void *arg, ...
          /* pid_t *ptid, void *newtls, pid_t *ctid */ );

/* For the prototype of the raw system call, see NOTES */

It can be seen that there are many parameters. The flags parameter here allows the caller to control how to share resources with the child process. Having this control right is the biggest difference between clone and fork:640?wx_fmt=png

#define _GNU_SOURCE

This means that clone is non-standard. Indeed, it is just a system call of Linux. The existence of this flexible clone call is entirely due to the flexible design of tasks at the bottom of the Linux kernel.

In traditional UNIX systems or UNIX-like systems, clone is not implemented. The reason for this may be that UNIX clearly defined processes from the beginning. Later, when UNIX had to support threads, it was necessary to introduce a  new concept of so-called  lightweight processes , which means processes that can share certain resources. . See the realization of lwp in the famous brand UNIX Solaris.

In these old Unix systems, the process concept that was too heavy at the beginning caused obstacles when introducing multithreading mechanisms. However, for Linux, it is completely unnecessary to introduce new data structures in order to support threads.

Although people often say that the clone call creates a lightweight process, it is just a name. There is no structure in the Linux kernel that represents a lightweight process.

The Linux kernel's low-level task design and system call interface design are destined to be super simple to implement the Posix thread specification. A clone parameter can be done: 640?wx_fmt=png"(since Linux 2.4.0)"  annotation, which means that before the 2.4 kernel, the Linux kernel does not support Posix threads. However, it is not supported here. It is just that the semantics required by the Posix specification for threads must not be implemented at the kernel level. It does not mean that it is not supported on the parallel multiprocessing mechanism. As for the semantics of POSIX threads, support in user mode is also a way. It's all before the 2.4 kernel.

After the 2.4 kernel, Linux's support for threads is completely at the kernel level. The pthread library is completely implemented based on CLONE_THREAD. See the clone manual shown in the figure above for the notes of CLONE_THREAD.

How to create a thread specifically? What happened at the bottom? See the simplest demo below:

#include <pthread.h>
#include <stdio.h>

void *func(void *unused)
{
	printf("sub thread\n");
	return (void *)123;
}

int main(int argc, char **argv)
{
	pthread_t t1;
	void *p;

	pthread_create(&t1, NULL, *func, NULL);
	pthread_join(t1, &p);
	printf("main thread:%d\n", (int)p);
	return 0;
}

Regarding threads, there are two important points, namely creation and destruction. Let's strace it:640?wx_fmt=png

  • Yellow: Indicate which resources are shared, MM, FILES, FS, etc.

  • Red: Realize the semantics of POSIX threads, such as sharing process PID and signal transmission.

After clone, a thread is created. After the thread executes func, it exits. The question is, how does the thread exit?

For ordinary C programs, we know that the main function returns to the C library, and the C library calls exit to exit the program after main returns. For multithreaded programs, when compiling the code, we explicitly link libpthread, which is similar to C The libpthread library takes care of things in a multithreaded program.

The approximate pthread_create should look like this:

void clone_func(Thread *thread)
{
	ret = thread->fn(...);
	exit(ret);
}
int pthread_create(..., fn, ...)
{
	thread = malloc(sizeof(&thread));
	thread->fn = fn;
	ret = clone(clone_func, &thread);
	return ERR_NO(ret);
}

We can see from the strace above that the thread exits using the exit system call, while the main process exits using the exit_group system call. The difference between the two is more in the semantics of the Posix process/thread. Strictly speaking, the exit system call Only exit the current task_struct, while exit_group exits all task_structs of the process where the current task_struct is located. For multi-threaded programs, it certainly exits all threads.

This is the realization principle of Linux kernel-level threads.

However, the clone system call is far more than a single realization of multi-threading, it can also optimize another level of UNIX fork. According to the utility of traditional UNIX fork at two levels, the corresponding description of Linux clone is as follows:

  1. At the level of executing new processes, clone can implement lightweight process fast exec only with CLONE_VM to avoid unnecessary resource copying.

  2. At the level of parallel multiprocessing, as mentioned earlier, clone CLONE_XX combined with CLONE_THREAD can implement kernel-level POSIX threads.


This article is a post-post about fork, let alone that fork is not, the idea of ​​fork was finally inherited and carried forward by Linux, and everything returned to Conway's original paper in 1963, parallel multiprocessing, and finally obtained on Linux clone system call To implement:

  • Clone can create a multi-threaded parallel execution sequence.

  • Clone creates a new process and reduces unnecessary resource duplication.

Well, this is the "fork"  story I want to tell you  .


The leather shoes in Wenzhou, Zhejiang are wet, so they won’t get fat in rain.

(Finish)

More exciting, all in "Linux reading field", scan the QR code below to follow

Your repost or click to see is our greatest support!

Guess you like

Origin blog.csdn.net/juS3Ve/article/details/101086067