[Linux] Stop 16: Process Address Space

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.

image-20231112194627813

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

image-20231112194300721

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.

image-20231112194952182

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.

image-20231112195251673

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

image-20231112195745479

The running result is

image-20231112195836108

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

image-20231112201716519

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

image-20231112202333121

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

image-20231112191449140

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.

image-20231113131310937

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

image-20231113132045274

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

image-20231113134011395

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.

image-20231113134932449

Finally, directly modify the data of the new physical memory

image-20231113135101749

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?

  1. 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

  2. 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.

    image-20231113154759135

    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**

    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

    image-20231113161025023

    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.

image-20231113163336374

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?

  1. 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)

  2. 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.)

  3. 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)

image-20231113170140089

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

image-20231113170957310

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.

image-20231113171730923

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

image-20231113171916621

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.

image-20231113173643245

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!

image-20231113175152748

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

image-20231113183933985

We can see that the addresses of the command line parameters are all on the stack.

image-20231113184058602

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.

syntaxbug.com © 2021 All Rights Reserved.