In the past few days, I took a look at Robert Love's Linux kernel design and implementation (of course, I am reading the Chinese translation version. Process management, scheduling, system scheduling, and interruption. I think this book is really well written. It seems that it is not so difficult for people like me, unlike the Linux kernel source code scenario analysis when I first came into contact with Linux, I saw that I was "in the fog". Now I think it's really a big fat man, before learning to walk, I started to run. Well, let's talk nonsense. Here we will review the process management chapter and hope that interested people can learn it!
1 concepts
What is a process?
A process is a process that processes the execution period. However, a process is not limited to a piece of executable code. It usually includes other resources, such as data segments for storing global variables, opened files, and suspended signals, of course, it also includes the address space and one or more execution threads.
What is thread
A thread is an activity object in the process. (Good abstraction ). Each thread has an independent program counter, process stack, and a set of process registers. The kernel scheduling object is a thread, not a thread. Currently, most systems support multi-threaded applications. As you can see later, there is no special distinction between thread implementation and processes in the Linux kernel.
Create Process
Note that the program is not a process. A process is a program in the execution period and the resources it contains. In fact, two or more different processes may actually execute the same program. They can also share resources such as open files and address spaces. In a Linux system, call the fork () System Call (write an implementation mechanism for Linux system call next time). The system call copies an existing process to create a new process. The process that calls fork () is called the parent process, and the newly generated process is a child process. At the same location as the return point, the parent process resumes execution and the child process starts execution. Generally, a new evil process is created to execute different programs, and the exec () function is called;
Process exited
The program exits the execution by calling the exit () system. This function terminates the process and releases the occupied resources. The parent process can be implemented through wait4 () (the kernel is responsible for implementing
Wait4 () System Call. In Linux, the wait () waitpid () wait3 () wait4 () function is usually provided through the C library. Next time we introduce the implementation of the system call, we will understand the relationship between the C library and the system call) the system calls to query whether the sub-process is terminated, which makes the process have the ability to wait for the execution of a specific process to complete. After the process exits, it is set to dead until its parent process calls wait () or waitpid.
Task
We often hear the concept of a task. In fact, the Linux kernel usually calls a process also a task. We usually call a program running in the kernel a task and a program running in the user space a process.
2. Process Descriptor and task queue
The kernel stores processes in a two-way cyclic linked list called a task list. Each item in the linked list is a structure of task_struct called process Descriptor (process decriptor), which is defined in the include/Linux/sched. h file. The process descriptor contains all information about a specific process.
Task_struct
Task_struct is relatively large. It is about KB On 32-bit machines. However, it contains all information about a process managed by the kernel. The data contained in it can fully describe a program being executed: it opens files, process address space, suspended signals, Process status, and so on.
Struct task_struct
{
Volatile long state;/*-1 unrunnable, 0 runnable,> 0 stopped */
Void * stack;
Atomic_t usage;
Unsigned int flags;/* per process flags, defined below */
Unsigned int ptrace;
Int lock_depth;/* BKL lock depth */
....
}
In Linux, The task_struct structure is allocated through the slab distributor to achieve Object reuse and caching (avoiding resource consumption caused by Dynamic Allocation and release ). In the kernel 2.6, The task_struct of each process is stored at the end of the kernel stack. In this way, we can use the stack pointer to calculate their positions. In linux, a new structure struct thread_info is created at the bottom or top of the stack: (defined in <asm/thread_info.h>)
Struct thread_info
{
Struct task_struct * task;/* main task structure */
Struct exec_domain * exec_domain;/* execution domain */
_ U32 flags;/* low level flags */
_ U32 status;/* thread synchronous flags */
_ U32 cpu;/* current CPU */
Int preempt_count;/* 0 => preemptable, <0 => BUG */
Mm_segment_t addr_limit;
Struct restart_block;
Void _ user * sysenter_return;
# Ifdef CONFIG_X86_32
Unsigned long previus_esp;/* ESP of the previous stack in case of nested (IRQ) stacks */
_ U8 supervisor_stack [0];
# Endif
Int uaccess_err;
}
Process Kernel stack (Note: each process has its own kernel stack. When a process enters the kernel state from the user State, the CPU automatically sets the kernel stack of the process)
------------------------------------------ Maximum memory address
|
| Stack header |
|
|
|
|
|
|
|
| ----------------------------------------- | Stack pointer
|
|
|
|
| ----------------------------------------- |
|
|
| Struct thread_struct |
| --------------------------------------- | Lowest memory address current_thread_info ()
Thread info has a pointer to the process Descriptor
The thread_info structure of each task is allocated at the end of its kernel stack. In the structure, the task stores the actual task_struct pointer to the task.
I am a little confused about this. Can I simply use task_struct? Why do I need thread_info? Can't I put other thread_info information in task_struct ???
You can use current_thread_info ()-> task to return the current process information structure.
Process status
The state in the process descriptor describes the current state of the process. There are five states:
TASK_RUNNING run ------ the process is executable; it is either in progress or waiting for execution in the running Queue (we know that if the cpu is single core, only one task can be executed at a time, other tasks are waiting for execution in the queue)
TASK_INTERRUPTIBLE can be interrupted-the process is sleeping, waiting for certain conditions to be met. Once these conditions are met, the kernel sets the Process status to run. This status will also be awakened and put into operation in advance because of receiving signals.
TASK_UNINTERRUPTIBLE cannot be interrupted-the status is the same except that it will not be put into operation because it receives signals. Use less
TASK_ZOMBIE dead ------- the process is finished, but the parent process has not yet called wait4 () System Call. In order for the parent process to know its message, the process descriptor of the child process remains
TASK_STOPPED stop ------- the process stops running. The process is neither running nor running. Generally, when receiving signals such as sigstop sigstp sigttin sigttou
Set the status of the current process
Use the set_current_state (state) or set_task_state (curent, state) function. As we mentioned above, current_thread_info ()-> the task can easily obtain the current process descriptor. Why not directly obtain the task-> state = state settings?
That's because in SMP (multi-core) systems, protection is required to prevent other processors from re-sorting (process running Queue );
Process context
This is a very important concept. When a program executes a system call or triggers an exception, it falls into the kernel space. In this case, we call the kernel "code process execution" and process context. In this context, the current macro is valid. Unless more advanced processes seize the execution of the current process, the program resumes execution in the user space when the kernel exits.
System calls and exception handling programs are clearly defined interfaces for the kernel. Only through these interfaces can a process fall into the kernel.
There is a clear inheritance relationship between Linux processes. All processes are descendants of the INIT process with PID 1. The kernel starts the INIT process at the final stage of system startup. This process reads the system initialization script and runs the relevant programs to complete the whole process of system startup.
Each process has a parent process, and each process can have one or more child processes. All processes with the same parent process are brothers. The relationship between processes is stored in the process descriptor. Each task_struct contains a parent process pointer and a Children sub-process linked list.
Struct task_struct * task = Current-> parent;/* obtain the parent process of the current process */
Struct list_head * List;
You can access sub-processes in sequence using the following methods:
Struct task_struct * task;
Struct list_head * List;
List_for_each(List, & Current-> children)
{
Task =List_entry(List, struct task_struct,Sibling)/* Sibling is also a member of the Process descriptor. The kernel explains this as follows: Linkage in my parent's children list, the sub-process linked list of the parent process, that is, the sibling process linked list of the current process */
}
The process descriptor of the INIT process is statically allocated as init_task. (It is actually a static variable)
The following code obtains the relationship between all processes.
Struct task_struct * task;
For (task = current; task! = & Init_task; task = task-> parent)
{
;
}
As you can see above, you can find any other process specified by any process in the system. But we don't need to be so troublesome, as long as we simply traverse all the processes in the system, because the task queue is a two-way circular linked list. As follows:
For a given process, obtain the next process of the linked list:
List_entry (Task-> tasks. Next, struct task_struct, tasks)
(What does this mean? Think back to the previous process descriptor structure. Is there a member list_head tasks in the structure. List_entry is a macro. It can obtain the address of struct task_struct from task> tasks. Next of its member tasks.
I do not know whether it is clear or not ,..)
Similarly, obtain the previous process:
List_entry (Task-> tasks. Prev, struct task_struct, tasks)
Similarly, the for_each_process (task) macro provides the ability to access this task in sequence:
Struct task_struct * task;
For_each_process(Task)
{
}
However, in a system with a large number of processes, it is very time-consuming to traverse all processes using this method, so there is no good reason not to do so.
3. Process Creation
Copy-on-write)
At the beginning, we introduced the process creation. The traditional fork () system calls directly copy all resources to the newly created process. This implementation is too simple and inefficient. Use of fork () in linuxCopy-on-write)Page implementation
Copy at write time is a technology that can delay or even avoid copying data. The kernel does not copy the whole process address space. Instead, it allows the parent process and the child process to share the same copy. Data is copied only when the write is required, so that each process has its own copy.
The actual overhead of fork () is to copy the page table of the parent process and create a unique process descriptor for the child process.
Fork ()
Linux uses the clone () system call to implement fork (). This call uses a series of parameter flags to specify the resources that the parent and child processes need to share. The fork (), vfork (), and _ clone () library functions call clone () according to their respective needs ().
Then, clone () calls do_fork ().
Do_fork completes most of the work in creation. It is defined in kernel/fork. c. This function calls the copy_process function and then starts the process to run.
Copy_process:
(1) Call dup_task_struct () to create a kernel stack, thread_info, and task_struct for the new process. These values are the same as those of the current process. At this time, the parent and child process descriptor is identical;
(2) check whether the number of newly created sub-processes exceeds the limit;
(3) many sub-process descriptors are set to 0 or an initial value to distinguish parent processes;
(4) set the sub-process status to TASK_UNINTERRUPTIBLE to ensure that it will not be put into operation;
(5) Call copy_flags () to update the flags member of task_struct. The PF_SUPERPRIV indicates whether the process has superuser permissions. Indicates that the PF_FORKNOEXEC flag of the exec () function has not been called by the process;
(6) Call get_pid () to obtain a valid PID for the new process;
(7) Copy resources based on the parameter flag passed to clone;
(8) Let the Parent and Child processes divide the remaining time slices equally
(9) returns a pointer to a sub-process.
Return to the do_fork () function. If the copy_process () function is returned successfully, the newly created sub-process is awakened and put into operation. The kernel intentionally selects the sub-process to be executed first. Generally, sub-processes call the exec () function immediately to avoid additional overhead of copy during write,
Vfork ()
Vfork is an optimization of the page copy performance of fork when writing. It is useless when fork is implemented. It is not introduced here;