Article directory
- 1. Program address space
-
- 1. Memory distribution
- 2. Why is it not released after static modification?
- 3. A strange phenomenon
- 2. Process address space
-
- 1. The reason for the previous phenomenon
- 2. What exactly is an address space?
- 3. Why is there a process address space?
- 4.Page table
- 5. What is process?
- 6. The process is independent. Why? How to do it?
- 3. Address of command line parameters
1. Program address space
1. Memory distribution
As shown in the figure below, it is the memory distribution we were familiar with before.
We also know that if it is a 32-bit machine, its space is 4GB, so is this thing memory?
In fact, it is wrong to call it memory.
We call this thing address space
Let’s first use the following code
#include<stdio.h> #include<stdlib.h> int g_val_1; int g_val_2 = 100; int main() { printf("code addr:%p\ ",main); const char* str = "hello world"; printf("read only string addr:%p\ ",str); printf("init global value addr:%p\ ", & amp;g_val_2); printf("uninit global value addr:%p\ ", & amp;g_val_1); char* mem = (char*)malloc(100); printf("heap:%p\ ",mem); printf("stack:%p\ ", & amp;str); return 0; }
The final running result is as follows
We found that these addresses are exactly at the bottom of the order, which exactly meets our address space distribution above.
Let us now verify that the address of the stack area is constantly decreasing, while the address of the heap area is increasing. Use the following code
#include<stdio.h> #include<stdlib.h> int g_val_1; int g_val_2 = 100; int main() { printf("code addr:%p\ ",main); const char* str = "hello world"; printf("read only string addr:%p\ ",str); printf("init global value addr:%p\ ", & amp;g_val_2); printf("uninit global value addr:%p\ ", & amp;g_val_1); char* mem = (char*)malloc(100); printf("heap addr:%p\ ",mem); printf("stack addr:%p\ ", & amp;str); printf("stack addr:%p\ ", & amp;mem); int a; int b; int c; printf("stack addr:%p\ ", & amp;a); printf("stack addr:%p\ ", & amp;b); printf("stack addr:%p\ ", & amp;c); return 0; }
The running results are as follows. We found that the address is indeed gradually decreasing.
Let’s verify again that the heap area grows in the direction of increasing address.
#include<stdio.h> #include<stdlib.h> int g_val_1; int g_val_2 = 100; int main() { printf("code addr:%p\ ",main); const char* str = "hello world"; printf("read only string addr:%p\ ",str); printf("init global value addr:%p\ ", & amp;g_val_2); printf("uninit global value addr:%p\ ", & amp;g_val_1); char* mem = (char*)malloc(100); char* mem1 = (char*)malloc(100); char* mem2 = (char*)malloc(100); printf("heap addr:%p\ ",mem); printf("heap addr:%p\ ",mem1); printf("heap addr:%p\ ",mem2); printf("stack addr:%p\ ", & amp;str); printf("stack addr:%p\ ", & amp;mem); int a; int b; int c; printf("stack addr:%p\ ", & amp;a); printf("stack addr:%p\ ", & amp;b); printf("stack addr:%p\ ", & amp;c); return 0; }
The running results are as follows. You can see that the address is indeed gradually increasing.
We can also find that the address gap between the stacks is very large, and there is a large space in the middle that is hollow. We will discuss this in detail later
2.Why is it not released after static modification
We have said before that local variables modified by static will not be released when the function ends, so why is this?
We can print its address
The running result is
We can see that during compilation, the static-modified variable has been compiled into the global data area, so it will not be released with the call of the function because it is already equivalent to a global variable.
3. A strange phenomenon
When we run the following code
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int g_val = 100; int main() { pid_t id = fork(); if(id == 0) { while(1) { printf("i am child,pid: %d,ppid: %d,g_val = %d, & amp;g_val = %p\ ",getpid(),getppid(),g_val, & amp;g_val); sleep(1); } } else { while(1) { printf("i am parent,pid: %d,ppid: %d,g_val = %d, & amp;g_val = %p\ ",getpid(),getppid(),g_val, & amp;g_val); sleep(1); } } return 0; }
The running results are as follows
We didn’t find anything wrong with this phenomenon.
But when we change the code to the following
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int g_val = 100; int main() { pid_t id = fork(); if(id == 0) { int cnt = 5; while(1) { printf("i am child,pid: %d,ppid: %d,g_val = %d, & amp;g_val = %p\ ",getpid(),getppid(),g_val, & amp;g_val); sleep(1); if(cnt) cnt--; else { g_val = 200; printf("Subprocess: change g_val 100 --> 200"); cnt--; } } } else { while(1) { printf("i am parent,pid: %d,ppid: %d,g_val = %d, & amp;g_val = %p\ ",getpid(),getppid(),g_val, & amp;g_val); sleep(1); } } return 0; }
The running result is as shown below
At this time we discovered a very strange phenomenon, that is when the data of the child process was changed from 100 to 200.
The value of the child process is indeed 200, and the value of the parent process is still 100, but the different values are actually in the same space?
According to our common sense, how can the same variable and the same address be read at the same time and read different contents? ! ! ! Shouldn’t it be copy-on-write?
So we can think of:If the address of the variable is a physical address, the above phenomenon cannot exist! !
So this address is definitely not a physical address. In fact, we generally call this address a linear address or a virtual address
In fact, like the C/C++ we usually write, the pointers used and the addresses in the pointers are not physical addresses! ! !
2. Process address space
1. The reason for the previous phenomenon
We already know that when we run a program, its PCB, the task_struct structure, will be created. In fact, in addition, a process address space will also be created
As shown below
In fact, there will be a pointer in our task_struct pointing to the process address space, which will establish a mapping relationship with the actual physical memory through a page table.
If we say that the virtual address of one of our initialized global variables is 0x601054, then it will find the actual physical address through the page table
When a child process is created, because the process is independent, it also creates its own PCB, process address space, and page table. We can understand that this page table is a direct copy of the parent process
As shown below, it will copy a page table, or use the same page table. In short, as long as the contents are the same, a mapping relationship can be established to map all virtual addresses to physical addresses. This allows code and data to be shared
When our child process performs the operation of g_val = 200, the physical physical memory will re-open a space, copy the original data, and then change the page table.
Finally, directly modify the data of the new physical memory
This is copy-on-write. It is done automatically by the operating system
Copy-on-write re-opens space, but during this process, the virtual address on the left is 0-aware, so you don’t care and will not affect it
So now, we have answered the previous question, why is the same address printed, but two different values?
2. What exactly is an address space?
-
What is an address space
We know that in a 32-bit computer, there are 32-bit address and data buses
Each bus has only two states: 0 and 1, and 32 buses have 2^32 states.
So 2^32 * 1byte = 4GB
So the address range [0,2^32) formed by our address bus arrangement and combination is the address space
-
How to understand the regional division of address space?
We can give an example
Just like our deskmates in elementary school, we often divide areas into areas, which we usually call line 38.
The so-called 38 lines are essentially regional divisions.
We can use a structure to describe them
struct area { int start; int end; }; struct destop_area//The agreed maximum range is 100 { struct area xiaoming; struct area xiaohua; }; int main() { struct destop_area line_area = {<!-- -->{1,50},{51,100}}; }
Or we can directly use a structure to describe
struct destop_area { int start_xiaoming; int end_xiaoming; int start_xiaohua; int end_xiaohua; };
So how do you understand the so-called spatial area adjustment, becoming larger or smaller? ? ?
We still use the previous example. When a Xiao Ming crosses the boundary, Xiao Hua beats Xiao Ming up, and asks Xiao Ming to cede the land to compensate. This is the adjustment of the space area.
line_area.xiaoming.end -= 10; line_area.xiaohua.start -= 10;
In this case, it is an adjustment of the spatial area.
So now they have their own space, for example, Xiao Ming’s area is [1,50]
Suppose now that Xiao Ming has obsessive-compulsive disorder, he divides his area into 50 parts, and each part places fixed things.
For example, if the pencil is placed in area 2
When someone borrows a pencil from him, he can go directly to the area to find the target object.
So it is not only necessary to divide the range of the address space for Xiao Ming, within this range, in the continuous space, every smallest unit can have an address, and this address can be directly used by Xiao Ming! ! !
Therefore **The so-called process address space is essentially a size that describes the visible range of the process. There must be various regional divisions in the address space. Just start and end the linear address font>**
Sothe essence of the address space is a data structure object of the kernel. Similar to the PCB, the address space is also managed by the operating system: first describe, in the organization
struct mm_struct //The default partition area is 4GB { long code_start; long code_end; long read_only_start; long read_only_end; long init_start; long init_end; long unitit_start; long unitit_end; long heap_start; long heap_end; long stack_start; long stack_end; }
So as shown below, each corresponding task_struct has a pointer pointing to its corresponding divided area. Use this structure to divide the process address space
This is the process address space
3. Why is there a process address space?
Let’s give an example
As shown in the figure below, suppose there is an old American, it is a rich man, it has 1 billion US dollars
Now it has four illegitimate children, each unaware of the other’s existence. It draws a big pie for every illegitimate child, saying that after I die, you will inherit the 1 billion. So everyone thinks that they will have a fortune of one billion in the future.
As for the illegitimate child’s daily small expenses, this rich man will provide it.
But if the illegitimate child wants all the money at once, then the rich man will definitely scold the illegitimate child and then not give him any money. However, after the illegitimate child was rejected, he still believed that the money would still be his in the future.
In this example,Monopoly is the operating system, and these bastards are the processes.
And this big pie is the process address space
So each process has a process address space, which can see all memory. Just like a big cake
So why do we need a process address space?
Let all processes view the memory structure from a unified perspective (for example, when the code and data need to be suspended in the future, the actual physical address will change. If we want the memory we see to change, It would be too troublesome to change. After we have the process address space, we don’t care about the actual physical address. The overall view of memory is from the perspective of the process address space)
Increasing the process virtual address space allows us to add a conversion process when accessing memory. During this conversion process, our addressing request can be reviewed, so once an abnormal access occurs, it is intercepted directly and the request will not reach the physical Memory, protects physical memory. (Similar to, when we got the New Year’s money when we were young, in order to prevent us from being cheated by unscrupulous merchants, our mother would keep the money. When we need to spend money, we can just take it out from our mother, and we can add one. layer of protection.)
Because of the existence of address space and page table, the process management module and memory management module are decoupled! (More details below)
4.Page table
As follows
In our CPU, there is actually a cr3 register, which always saves the address of the page table (physical address)
So if our current process is switched away, we won’t worry about not finding this page table in the future.
Because the page table address i is the temporary data of the current process, it essentially belongs to the context of the process. So when this process switches in the future, this address will be taken away. When it comes back in the future, this data will be restored. So this page table can be found from beginning to end.
As shown below, when we have data in the future, we must establish such a mapping relationship
But our current problem is that we know that the string constant area and code area are read-only. But how does the operating system know whether this data is read-only or can be written? , how does it know whether our physical memory can be modified?
So in fact, the page table also has a flag bit. This flag can confirm whether it has been modified
As shown below, for global initialized variables, its permissions are readable and writable
The data in the code area is read-only.
So the page table can provide good permission management. Physical memory does not have permission management. You can write when you want and read when you want. This is all due to the page table setting permissions.
So for this code
We now know why this code will not be passed
Because character constants are read-only, all permissions in the page table are read-only. So the operating system will intercept us, so the code will hang. The reason is here
We know that the process can be suspended, so how do we know that the process has been suspended? How do we know whether our process code data is in memory?
There is a consensus here
Modern operating systems do almost nothing that wastes space or time
We know that when we load Genshin Impact, the memory will definitely be full, so the operating system must be able to load large files in batches. So you can load some larger files.
So the way our operating system is loaded is lazy loading. (For example, for a 500MB code, the operating system will not load all of it when it comes up, but only 5MB, because many of the subsequent codes will not be used temporarily)
So it is possible that in the page table, although the virtual address is there, the physical address may not be filled in for the time being, and in addition to the first three in the page table, there is also a field marked by the address that points to A specific address on disk or an address in memory. That is, whether the corresponding code and data have been loaded into memory.
So in this case, when we access the page table, we first look at the flag bit corresponding to the virtual address, that is, to see whether the code and data have been loaded into the memory. If it is already loaded, read it directly. If it is not loaded, a page fault interrupt will occur in our operating system at this time. We first find the data of the corresponding executable program, and then load these data into the memory. Then fill in the address of this memory into the physical address. Then resume the access process at that time. You can now access it normally.
So in extreme cases, even if we create the process, none of the data and code can be loaded at all, and can be loaded slowly and lazily. At this time, it is loaded while using it. But in fact this will not be the case. Generally speaking, part of it will be loaded.
So when a process is created, the kernel data structure is created first? Or should we load the corresponding program first?
We also have the answer to this question. The answer is to create the kernel data structure first. Then slowly load the executable program.
But I have said so much about memory before, so what kind of memory should I apply for? Where to apply for memory? What part of the executable program is loaded when loading? How much to load? Where is it loaded into physical memory? How to fill in the physical address into the page table? When should I fill it out?
Who will do this? It’s all done by memory! The above are all Linux memory management modules, we will talk about them later!
For our process, we don’t care about the entire process of applying for memory, releasing memory, including page fault interrupts, and re-application. It doesn’t know about it and doesn’t need to take care of it.
So it is precisely because of the existence of page tables. We can divide it into process management and memory management!
It is precisely because of the existence of the page table that the process no longer needs to care about memory!
So the existence of the virtual process address space decouples process management and memory management at the software level!
In this case, it doesn’t matter when it is loaded into the physical memory or where it is loaded into the physical memory. Because of the page table mapping, the physical memory can be completely out of order, and the left side can still be presented to the user in a linear manner. Disorder directly changes to order
5. What is process?
Now we have a deeper understanding of the process
Process = kernel data structure (task_struct & mm_struct & page table) + program code and data
As long as the PCB of the process is switched, the process address space is automatically switched. Because PCB points to this process address space. And because the cr3 register belongs to the context of the process, the process context is switched and the page table is automatically switched.
6. The process is independent. Why? How to do it?
One: Because each process has a PCB table, process address space, and page table, the kernel data structure is independent.
Therefore, the parent and child processes have independent kernel data structures.
Second: It is also reflected in the memory and data that have been loaded. It only needs to be exactly the same at the virtual address of the page table, but different at the physical address. As long as the page table is mapped to different areas of physical memory, the code and data will be decoupled from each other. Even if it is a parent-child relationship, as long as the code area points to the same point and the data area is different, it is decoupled at the data level. In this way, if you release yourself, it will not affect others.
3. Address of command line parameters
We use the following code
#include <stdio.h> #include <stdlib.h> int g_val_1; int g_val_2 = 100; int main(int argc, char* argv[], char* env[]) {<!-- --> printf("code addr:%p\ ",main); const char* str = "hello world"; printf("read only string addr:%p\ ",str); printf("init global value addr:%p\ ", & amp;g_val_2); printf("uninit global value addr:%p\ ", & amp;g_val_1); char* mem = (char*)malloc(100); char* mem1 = (char*)malloc(100); char* mem2 = (char*)malloc(100); printf("heap addr:%p\ ",mem); printf("heap addr:%p\ ",mem1); printf("heap addr:%p\ ",mem2); printf("stack addr:%p\ ", & amp;str); printf("stack addr:%p\ ", & amp;mem); static int a = 0; int b; int c; printf("stack addr:%p\ ", & amp;a); printf("stack addr:%p\ ", & amp;b); printf("stack addr:%p\ ", & amp;c); int i = 0; for(; argv[i]; i + + ) {<!-- --> printf("argv[%d]:%p\ ",i,argv[i]); } for(i = 0; env[i]; i + + ) {<!-- --> printf("env[%d]:%p\ ",i,env[i]); } return 0; }
The running results are as follows
We can see that the addresses of the command line parameters are all on the stack.
Therefore, the command line parameters are neither in the code area nor the data area. They have their own independent area, above the stack area.
When creating a child process, why can the child process inherit the environment variables of the parent process?
Because when the child process starts, the parent process has already loaded the environment variables.
The environment variables of the parent process are also data in the address space of the parent process.
The parent process must have a page table mapping from virtual to physical addresses.
So when the child process is created, the child process has already established this mapping.
Therefore, even if it is not passed on, the corresponding parameters and sub-processes can still obtain the corresponding environment variable information.
This is why environment variables have global attributes and will be inherited by the child process, because its data can be directly found by the child process through the page table.
Secondly, we can also see that in the address space, the user is 3GB, and 1GB is the kernel space, which is for the operating system.
Therefore, our PCB, including data structure objects such as process address space, will be placed in physical memory in the future. These data structures are the data structures of the operating system and must be mapped into the 1GB of kernel space.
So what we said above is the user’s space.