Linux multi-process development

Linux multi-process development directory

process

procedures and processes

what is program

A program is a file that contains a set of information that describes how to create a process at run time.

information file

  • Binary format identification : Each program file contains metainformation that describes the format of the executable file. The kernel uses this information to interpret other information in the file. (ELE executable connection format)
  • Machine language instructions : encoding program algorithms
  • Program entry address : identifies the starting instruction location when the program begins execution.
  • Data : The program file contains the initial values ​​of variables and literal values ​​used by the program (such as strings).
  • Symbol table and relocation table : describe the locations and names of functions and variables in the program. These tables serve multiple purposes, including debugging and runtime symbol resolution (dynamic linking).
  • Shared libraries and dynamic link information : Some fields contained in the program file list the shared libraries that need to be used when the program is running, as well as the path name of the dynamic linker that loads the shared libraries.
  • Other information : Program files also contain a lot of other information that describes how to create the process.

what is process

A process is an instance of a running program . It is a running activity of a program with certain independent functions on a certain data set. It is the basic unit of dynamic execution of the operating system . In traditional operating systems, processes are both the basic execution unit and It is also the basic distribution unit. A process is the smallest unit of system resource allocation. A process can have multiple threads.

The relationship between process and kernel
A program can be used to create multiple processes. A process is an abstract entity defined by the kernel, and various system resources used to execute the program are allocated to this entity.
From the perspective of the kernel, the process consists of user memory space and a series of kernel data structures. The user memory space contains program code and variables used by the code, while the kernel data structure is used to maintain process status information.
The information recorded in the kernel data structure includes many process-related identification numbers (IDs), virtual memory tables, open file descriptor tables, information about signal transmission and processing, process resource usage and limitations, the current working directory and a large number of of other information.


Single-channel and multi-channel programming

What is Single Programming

Single-programming means that only one program is allowed to run in the computer memory during a period of time. Once a program starts to execute, other programs cannot execute until it completes execution or an error occurs.

What is multiprogramming

Multiprogramming means that within a period of time, the computer can load and execute multiple programs at the same time , so that multiple programs run interspersed with each other under the control of the management program and are in the same state from start to end in the computer system. Programs share computer system resources, thereby improving system resource and CPU utilization.

CPU processing program
For a single-CPU system, programs running at the same time is only a macro concept. Although they have all started running, from a micro perspective, there is only one program running on the CPU at any time . In the multiprogramming model, multiple processes take turns using the CPU. Today's common CPUs are at the nanosecond level and can execute approximately 1 billion instructions per second. Since the reaction speed of the human eye is on the order of milliseconds, they appear to be running simultaneously.


time slice

What is a time slice

A time slice is a microscopic period of CPU time allocated by the operating system to each running process .
When only one CPU is considered, these processes "appear" to be running at the same time, but in fact they are running alternately. Since the time slice is usually very short (generally a few ms to tens of ms on Linux), users will not feel it.

The role of time slices in the kernel
Time slices are assigned to each process by the scheduler of the operating system kernel . First, the kernel will allocate equal initial time slices to each process, and then each process will execute the corresponding time in turn. When all processes are in a state where the time slice is exhausted, the kernel will recalculate and allocate time to each process. Film, and so on.


Parallelism and Concurrency

What is parallelism

Parallel means that multiple instructions are executed simultaneously on multiple processors at the same time.

Insert image description here

What is concurrency

Concurrency means that only one instruction can be executed at the same time, but multiple process instructions are executed in rapid rotation, which has the effect of multiple processes executing at the same time from a macro perspective, but from a micro perspective, they are not executed at the same time . Time is divided into several segments, allowing multiple processes to execute alternately quickly.

Insert image description here

understand examples

Parallelism is two queues using two coffee machines at the same time:
Insert image description here

Concurrency is two queues using a coffee machine alternately:
Insert image description here


Process Control Block (PCB)

In order to manage processes, the kernel must have a clear description of what each process does. The kernel allocates a PCB (Processing Control Block) process control block to each process to maintain process-related information. The process control block of the Linux kernel is a task_structstructure.

task_structThe structure definition can be found in the following files :

/usr/src/linux-headers-xxx/include/linux/sched.h

Insert image description here

What you need to know about task_structthe structure content

  • Process id: Each process in the system has a unique id, represented by the pid t type, which is actually a non-negative integer.
  • Process status: ready, running, suspended, stopped, etc.
  • Some CPU registers that need to be saved and restored when switching processes
  • Information describing the virtual address space
  • Information describing the control terminal
  • Current Working Directory
  • umask mask
  • File descriptor table, containing many pointers to file structures
  • Information related to signals
  • user id and group id
  • Sessions and process groups
  • The upper limit of resources that a process can use (Resource Limit)

Command to view resource upper limit

ulimit -a

Insert image description here


Process state transition

process status

Process status reflects changes in process execution . These states transition as the process executes and external conditions change.

In the three-state model, the process state is divided into three basic states:

  • Running state : The process occupies the processor and is running.
  • Ready state : The process is ready to run and is waiting for the system to allocate a processor to run. After a process has been allocated all necessary resources except the CPU, it can be executed as soon as it gets another CPU. There may be multiple processes in the ready state in a system, and they are usually queued into a queue, called the ready queue.
  • Blocking state : Also known as wait state or sleep state, it means that the process does not have running conditions and is waiting for the completion of an event.

Insert image description here

In the five-state model, the process state adds new state and termination state to the three basic states.

  • New state : The state when the process has just been created and has not yet entered the ready queue.
  • Termination state : The state in which a process reaches a normal end point after completing a task, or terminates abnormally due to an insurmountable error, or is terminated by the operating system and a process with termination rights. The process that enters the terminated state will no longer be executed, but it will still remain in the operating system waiting for aftermath. Once other processes have completed extracting information about the terminated process, the operating system will delete the process.

Insert image description here

Process related commands

View progress

a- Shows all processes on the terminal, including processes of other users
u- Shows details of processes
x- Shows processes that do not have a controlling terminal
j- Lists information related to job control

ps aux

Insert image description here

ps ajx

Insert image description here

STAT parameter meaning
Insert image description here


Real-time display of process dynamics

top

You can add when using topthe command -d nto specify the time interval for updating the display information.

top -d 5

Insert image description here

Update sorting.
After entering the top interface, type the following keyboard keys to display according to the rules.

  • M: Sort by memory usage
  • P: Sort by CPU occupancy
  • T: Sort by process running time
  • U: Filter processes based on username
  • K: Enter the specified PID to kill the process
  • Q:Exit the top interface

kill process

Syntax :kill 信号选项 pid

list all signals

kill -l

Insert image description here

# 强制终止进程的命令
kill -9 pid #kill -SIGKILL pid

# 建议先向进程发送一个终止请求,允许进程执行清理工作并正常退出(15是kill默认信号)。别直接一来就9,除非出现严重问题。
kill -15 pid #kill -SIGTERM pid

Kill process by process name

killall name

Process ID and related functions

process number

Each process is identified by a process number (PID)pid_t , whose type is (integer). The range of the process number: 0~32767. The process ID is unique but can be reused. When a process terminates, its process ID can be used again.

Parent process number

Any process (except the init process) is created by another process, which is called the parent process of the created process. The corresponding process number is called the parent ID (PPID) .

process group number

A process group is a collection of one or more processes. They are related to each other. The process group can receive various signals from the same terminal. The associated process has a process group number (PGID) . By default, the current process number is used as the current process group number.

related functions

pid_t getpid(void);

  • Function : Used to obtain the process number of the calling process
  • Return value : If successful, the child process returns 0, and the parent process returns the child process ID and the process number of the calling process; if it fails, it returns -1 and sets errno.

pid_t getppid(void);

  • Function : used to obtain the parent process number of the calling process
  • Return value : If successful, the child process returns 0, the parent process number of the calling process; if failed, returns -1 and sets errno.

pid_t getpgid(void);

  • Function : used to obtain the process group number of the calling process
  • Return value : If successful, the process group number is returned to the child process; if failed, -1 is returned and errno is set.

Process creation

The system allows a process to create a new process, and the new process is a child process. The child process can also create a new child process to form a process tree structure model.

fork function

pid_t fork(void);

  • Function : Used to create a new process (child process)
  • Return value : If successful, 0 is returned in the child process, and the child process ID is returned in the parent process; if failed, -1 is returned and errno is set.

Two main reasons for failure

  1. The number of processes in the current system has reached the upper limit specified by the system. At this time, the value of errno is set to EAGAIN.
  2. The system has insufficient memory, and the value of errno is set to ENOMEM.

code example

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    
    

    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if (pid > 0)
    {
    
    
        printf("pid : %d\n", pid);
        // 如果大于0,返回的是创建的子进程的进程号,当前是父进程
        printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
    }
    else if (pid == 0)
    {
    
    
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

    for (int i = 0; i < 5; i++)
    {
    
    
        printf("i : %d, pid : %d\n", i, getpid());
        sleep(2);
    }

    return 0;
}

Insert image description here

ps: Let me put a controversial question here. The question originated from Qiniuyun’s written test question. The
answer to the question in 2018 is still being discussed by some people. Welcome all the big guys to come and discuss it.

The following program outputs () "-"

int main(void) {
     
     
    int i;
    for (i = 0; i < 2; i++) {
     
     
        fork();
        printf("-");
    }
    return 0; 
} 

A. 2        B. 4        C. 6        D. 8

A total of 8 prints are printed after GCC is compiled and run in the Linux environment.
Insert image description here

Change printf("-");to printf("-\n");and print 6 in the same environment
Insert image description here


Virtual address space situation of parent and child processes

Differences between parent and child process running

  1. When the parent process is executed fork(), a child process is created
  2. After the child process is created, fork()the PID of the child process will be returned to the parent process, and 0 will be returned to the child process.
  3. The system will generate a new virtual address space for the copied resources of the parent process for use by the child process.
  4. The parent process executes conditional judgment, and the child process executes conditional judgment (the child process only executes fork()the code after)
  5. By looping and sleep()explicitly reflecting the cpu's alternating handling of parent and child processes
    Please add image description

Parent-child process virtual address space

Expand step 3 of the previous section.
After calling fork(), the user area data of the child process is the same as the parent process but fork()the return value is different. The core area data will also be copied but the pid is different.
Insert image description here

code example

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    
    
    int num = 10;
    // 输出num原定义值
    printf("original num: %d\n", num);

    // 输出num原地址
    printf("Address of original num: %p\n", &num);

    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if (pid > 0)
    {
    
    
        // printf("pid : %d\n", pid);
        //  如果大于0,返回的是创建的子进程的进程号,当前是父进程
        printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());

        printf("parent num: %d\n", num);
        num += 10;
        printf("parent num += 10: %d\n", num);

        // 输出父进程中num的地址
        printf("Address of num in parent precess: %p\n", &num);
    }
    else if (pid == 0)
    {
    
    
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
        printf("child num: %d\n", num);
        num += 100;
        printf("child num += 100: %d\n", num);

        // 输出子进程中num的地址
        printf("Address of num in child precess: %p\n", &num);
    }

    // for (int i = 0; i < 5; i++)
    // {
    
    
    //     printf("i : %d, pid : %d\n", i, getpid());
    //     sleep(2);
    // }

    return 0;
}

Insert image description here
The final print result shows that the two address processes are the same. This is because what we understand is the virtual memory address. The virtual memory address is shared by each process, but the physical memory address mapped by mmu is different and will be copied through writing operations. data to the new physical memory address.
From the perspective of the operating system, each process has its own page table. When the parent process forks a new child process, the child process copies the page table of the parent process, and the parent and child processes change the page table status to write-protected. When a write operation occurs in the parent process or the child process, a page fault exception will occur, and the page fault exception handling function will allocate a new physical address to the child process. Since different processes have different page tables, the physical addresses corresponding to accessing the same logical address are different .


Copy-On-Write technology

When a process or thread wants to modify shared data, it first creates a copy of the data and then modifies it.
The original data remains unchanged, and other processes or threads can still read copies of the original data. This strategy avoids race conditions when multiple processes modify data at the same time, thereby improving concurrency performance.

The above fork()implementation is sharing when reading and copying when writing . When the resource is read, the kernel does not copy the address space of the entire process at this time, but allows the parent and child processes to share the same address space. Only when writing is needed, it will be copied to the new address space, so that each process has its own address space.

The class in the STL standard template library stringis a class with copy-on-write technology.
Usually stringthere must be a private member in the class, which is a char*user-recorded address of memory allocated from the heap. It allocates memory during construction and releases the memory during destruction.
Because memory is allocated from the heap, stringthe class is extra careful in maintaining this memory. stringWhen the class returns this memory address, it only returns const char*, which is read-only. If it needs to be written, it can only stringbe done through the provided method. Rewriting of data.

Notice

  1. After fork, the parent and child processes share files. The resulting child process has the same file descriptor as the parent process and points to the same file table. The reference count is increased and the file offset pointer is shared.
  2. Different gcc compilers have different handling strategies for shared memory. In some environments, it may be a direct deep copy, while in other environments it may be shared content.

Parent-child process relationship and GDB multi-process debugging

Parent-child process relationship

the difference

  • fork()The return value of
    • In the parent process: >0 returns the ID of the child process
    • In child process: =0
  • Some data in the PCB
    • The current process id: pid
    • The id of the parent process of the current process: ppid
    • signal collection

Common
When the child process has just been created and has not performed any data writing operations, the following objects are shared by the parent and child processes.

  • User area data
  • file descriptor table

Are variables shared between the parent and child processes?
They are shared at the beginning, but once the data is modified, they cannot be shared. Share when reading, copy when writing.


GDB multi-process debugging

There are generally no jobs and more interviews.

When using GDB for debugging, GDB can only track one process by default. You can fork()set the GDB debugging tool to track the parent process or track the child process through instructions before calling. The parent process is tracked by default.

code example

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(){
    
    
    printf("begin\n");
    if (fork()>0)
    {
    
    
        printf("我是父进程: pid = %d, ppid = %d\n", getpid(), getppid());
        int i;
        for(i = 0; i< 10; i++){
    
    
            printf("i = %d\n", i);
            sleep(1);
        }
    }else{
    
    
        printf("我是子进程: pid = %d, ppid = %d\n", getpid(), getppid());
        int j;
        for(j = 0; j< 10; j++){
    
    
            printf("j = %d\n", j);
            sleep(1);
        }
    }
    return 0;
}

Perform GDB debugging

# 查看源代码
l
# 设置父进程打印断点
b 9
# 设置子进程打印断点
b 16
# 运行
r

Insert image description here


Set debugging child or parent process

Set debugging parent process (default)

set follow-fork-mode parent

Insert image description here

Set up debugging subprocess

set follow-fork-mode child

Insert image description here
Perform GDB debugging
Insert image description here


Set debug mode

Debug the current process, other processes continue to run (default)

set detach-on-fork on

Insert image description here

Debugging the current process, other processes are suspended by GDB

set detach-on-fork off

Insert image description here
Perform GDB debugging
Insert image description here


View the debugging process

info inferiors

Insert image description here


Switch the currently debugged process

inferiors id

Insert image description here
Insert image description here


Taking a process out of GDB debugging

detach inferiors id

Insert image description here


exec function family

What is the exec function family

Execute an executable file inside the calling process.

The function of the exec function family is to find the executable file based on the specified file name and use it to replace the contents of the calling process.
Functions in the exec function family will not return after successful execution, because the entities of the calling process, including code segments, data segments, and stacks, have been replaced by new content, leaving only some superficial information such as process IDs as they are; Only if the call fails, -1 will be returned and execution will continue from the calling point of the original program.
Calling the exec function family does not create a new process but only replaces the data in the user area.

exec function family

#include <unistd.h>
extern char **environ;

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);

meaning distinction

  • Band l: Each command line parameter of the calling program must be written separately in list form and terminated by a null pointer.
  • Band p: If the function parameter filecontains , /it is regarded as the path name, otherwise the executable file is searched in the directory specified by the PATH environment variable
  • Band v: You need to construct a pointer array of command line parameters first, and then use the array address as the calling program parameter.
  • Band e: You need to construct an array of environment string pointers first, and then pass the array address to the function to use the new environment variables to replace the environment variables of the calling process.

Parameters
path : The path name of the executable file
file: Search for the executable file in the directory specified by the PATH environment variable
arg: The command line parameters of the executable program. The first parameter is the name of the executable file (it is useless, usually write this), Starting from the second parameter is the list of parameters required by the program, and must end with NULL
argv[]: pointer array of command line parameters
envp[]: array of environment string pointers

Return value will not be returned after successful
execution , because the entities of the calling process, including code segments, data segments, and stacks, have been replaced by new content, leaving only some superficial information such as the process ID remaining intact; only the call fails . Only then will it return -1 and set erron to continue execution from the calling point of the original program.

Code example
hello.c

#include <stdio.h>

int main(){
    
    
    printf("helloworld!\n");
    return 0;
}

Compiled into hello executable file

execl.c

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    
    
    // 创建一个子进程,在子进程中执行exec函数族中的函数
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        // 父进程
        printf("i am parent process, pid : %d\n", getpid());
        sleep(1);
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        // 建议绝对路径
        execl("/home/zxz/open_file_excise/exec_process/hello", "hello", NULL);
        printf("i am child process, pid : %d\n", getpid());
    }
    for (int i = 0; i < 3; i++)
    {
    
    
        printf("i = %d, pid = %d\n", i, getpid());
    }
    return 0;
}

Insert image description here


process control

Process exits

Insert image description here
status: It is a status information when the process exits. It can be obtained when the parent process recycles the resources of the child process.

code example

  1. Standard C library functionsexit()
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    
    
    printf("hello\n");
    printf("world");

    exit(0);
    // exit(0) 等价于 return 0,于是不再执行以下代码
    printf("byebye");

    return 0;
}

Insert image description here

  1. Standard Linux system library functions_exit()
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    
    
    printf("hello\n");
    printf("world");

    _exit(0);
    // exit(0) 等价于 return 0,于是不再执行以下代码
    printf("byebye");
    return 0;
}

Insert image description here
Notice: The I/O buffer will be refreshed before exit()calling _exit(). Since when std::endlor \nis output, the buffer will be refreshed, so the data will be displayed on the screen immediately. However, calling directly _exit()will not refresh the I/O buffer, so when std::endlor \nData will not be displayed on the screen when it is not output.

Orphan process

The parent process has ended, but the child process is still running (not ended). Such a child process is called an orphan process.

code example

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    
    
    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if (pid > 0)
    {
    
    
        //  如果大于0,返回的是创建的子进程的进程号,当前是父进程
        printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
    }
    else if (pid == 0)
    {
    
    
        sleep(1);
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

    for (int i = 0; i < 5; i++)
    {
    
    
        printf("i : %d, pid : %d\n", i, getpid());
    }

    return 0;
}

Insert image description here
There is no harm in the orphan process. The parent process (the kernel's init process) that has adopted the orphan process will cycle through the wait()child processes that have exited, and will eventually process the child process until it ends its life cycle.

zombie process

After each process ends, it will release the user area data in its own address space. The PCB in the kernel area cannot be released by itself and needs to be released by the parent process. When the process terminates, the parent process has not yet been recycled, and the residual resources (PCB) of the child process are stored in the kernel and become a zombie process (Zombie Process).

code example

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
    
    
    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if (pid > 0)
    {
    
    
        while (1)
        {
    
    
            //  如果大于0,返回的是创建的子进程的进程号,当前是父进程
            printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
            sleep(1);
        }
        
    }
    else if (pid == 0)
    {
    
    
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

    for (int i = 0; i < 5; i++)
    {
    
    
        printf("i : %d, pid : %d\n", i, getpid());
    }

    return 0;
}

After the parent process is executed, the child process is still executing without releasing the kernel resources (infinite loop).
Insert image description here
Create a new terminal input ps auxto view the zombie process.
Insert image description here
General debugging use Ctrl + C. Kill the zombie process ( kill -9cannot be killed).
Insert image description here

ps: If the parent process does not call wait()or waitpid(), then the retained information will not be released, and its process number will always be occupied, but the process number that the system can use is limited. If a large number of zombie processes are generated, it will If there is no available process number and the system cannot generate new processes, this is the harm of zombie processes and should be avoided.


Process recycling

When each process exits, the kernel releases all resources of the process, including open files, occupied memory, etc. However, certain information is still retained for it, which mainly refers to the information of the process control block PCB (including process number, exit status, running time, etc.). The parent process can get its exit status and clean up the process completely
by calling wait()or .waitpid()

wait function

pid_t wait(int *wstatus);

  • Function : Wait for any child process to end. If any child process ends, this function will recycle the resources of the child process.
  • Parameters : wstatus: Status information when exiting. An int type address is passed in, and parameters are passed out.
  • Return value : If successful, return the id of the recycled child process; if failed, return -1 (all child processes are terminated and the function call fails)

The calling wait()process will be suspended (blocked) until one of its child processes exits or receives a signal that cannot be ignored before being awakened (equivalent to continuing execution). If there are no child processes or all child processes have ended, -1 will be returned immediately.

code example

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

int main()
{
    
    
    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;
    for (int i = 0; i < 5; i++)
    {
    
    
        pid = fork();
        if (pid == 0)
        {
    
    
            break;
        }
    }
    if (pid > 0)
    {
    
    
        // 父进程
        while (1)
        {
    
    
            printf("parent, pid = %d\n", getpid());

            int ret = wait(NULL);
            if (ret == -1)
            {
    
    
                break;
            }

            printf("child die, pid = %d\n", ret);

            sleep(1);
        }
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        while (1)
        {
    
    
            printf("child, pid = %d\n", getpid());
            sleep(1);
        }
    }

    return 0;
}

After executing the code, use ps aux to check that the 5 child processes created are all blocked.
Insert image description here
Select the first child process to kill.

kill -9 84790

Insert image description here

Exit information related macro functions

quit

  • WIFEXITED(status): Non-0, the process exits normally
  • WEXITSTATUS (status): If the above macro is true, get the process exit status ( exit()parameters)

termination

  • WIFSIGNALED(status): Non-0, the process terminates abnormally
  • WTERMSIG(status): If the above macro is true, get the signal number that caused the process to terminate.

pause

  • IFSTOPPED (status): Non-0, the process is in suspended state
  • WSTOPSIG(status): If the above macro is true, get the number of the signal that caused the process to pause.
  • WIFCONTINUED (status): Non-0, the process has continued to run after being suspended.

code example

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
    
    
    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;
    for (int i = 0; i < 5; i++)
    {
    
    
        pid = fork();
        if (pid == 0)
        {
    
    
            break;
        }
    }
    if (pid > 0)
    {
    
    
        // 父进程
        while (1)
        {
    
    
            printf("parent, pid = %d\n", getpid());

            // int ret = wait(NULL);
            int st;
            int ret = wait(&st);
            
            if (ret == -1)
            {
    
    
                break;
            }
            if (WIFEXITED(st))
            {
    
    
                // 是不是正常退出
                printf("退出的状态码:%d\n", WEXITSTATUS(st));
            }
            if (WIFSIGNALED(st))
            {
    
    
                // 是不是异常终止
                printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
            }
            printf("child die, pid = %d\n", ret);
            sleep(1);
        }
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        while (1)
        {
    
    
            printf("child, pid = %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }

    return 0;
}

Kill child process using signal
Insert image description here
Insert image description here

waitpid function

pid_t waitpid(pid_t pid, int *wstatus, int options);

  • Function : Recycle a child process with a specified process number, and you can set whether to block.
  • Parameters :
    • pid
      • pid > 0: pid of a child process
      • pid = 0: Recycle any child process of the current process group
      • pid = -1: Recycle all child processes, equivalent to wait()
      • pid < -1: The absolute value of the group ID of a certain process group, recycling the child processes in the specified process group
    • wstatus: Status information when exiting, passing in an address of type int, passing out parameters
    • options: Set blocking or non-blocking
      • 0:block
      • WNOHANG: non-blocking
  • Return value :
    • >0:returns the id of the child process
    • =0: options= WNOHANG, indicating that there are still child processes alive
    • = -1:Error, or there is no child process

code example

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
    
    
    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;
    for (int i = 0; i < 5; i++)
    {
    
    
        pid = fork();
        if (pid == 0)
        {
    
    
            break;
        }
    }
    if (pid > 0)
    {
    
    
        // 父进程
        while (1)
        {
    
    
            printf("parent, pid = %d\n", getpid());
            sleep(1);

            // int ret = wait(NULL);
            int st;
            // int ret = waitpid(-1, &st, 0);
            // 非阻塞,父进程不用挂起还可以执行
            int ret = waitpid(-1, &st, WNOHANG);
            if (ret == -1)
            {
    
    
                break;
            }
            if (ret == 0)
            {
    
    
                // 说明还有子进程存在
                continue;
            }
            else if (ret > 0)
            {
    
    
                if (WIFEXITED(st))
                {
    
    
                    // 是不是正常退出
                    printf("退出的状态码:%d\n", WEXITSTATUS(st));
                }
                if (WIFSIGNALED(st))
                {
    
    
                    // 是不是异常终止
                    printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
                }
            }

            printf("child die, pid = %d\n", ret);
                }
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        while (1)
        {
    
    
            printf("child, pid = %d\n", getpid());
            sleep(1);
        }
        exit(0);
    }

    return 0;
}

Insert image description here
Kill child process using signal
Insert image description here
Insert image description here


interprocess communication

What is inter-process communication

Inter-Process Communication (IPC) refers to a mechanism for data exchange and information sharing between different executing processes .
A process is an independent resource allocation unit. The resources between different processes (the processes mentioned here usually refer to user processes) are independent and not related. One process cannot directly access the resources of another process. However, processes are not isolated. Different processes need to interact with information and transfer status, so inter-process communication is required.

Inter-process communication purpose

  • Data transfer : One process needs to send its data to another process.
  • Notification event : A process needs to send a message to another process or a group of processes to notify it (them) that a certain event has occurred (such as notifying the parent process when the process terminates).
  • Resource sharing : sharing the same resources between multiple processes. In order to do this, the kernel needs to provide mutual exclusion and synchronization mechanisms.
  • Process control : Some processes hope to completely control the execution of another process (such as the Debug process). At this time, the control process hopes to intercept all traps and exceptions of another process and be able to know its status changes in time.

Linux inter-process communication method

Insert image description here

anonymous pipe

Pipes are also called nameless (anonymous) pipes. They are the oldest form of IPC (inter-process communication) in UNIX systems. All UNIX systems support this communication mechanism. An anonymous pipe is a one-way communication pipe that allows one process to write data to one end of the pipe and another process to read data from the other end of the pipe .

Code example
counts the number of files in a directory. In order to execute this command, the shell creates two processes to execute lsandwc

ls | wc -l

Insert image description here

Characteristics of pipelines

  • The pipeline is actually a buffer maintained in the kernel memory . The storage capacity of this buffer is limited, and different operating systems may not necessarily have the same size.
  • Pipes have the characteristics of files: read operations and write operations. Anonymous pipes have no file entities. Named pipes have file entities, but do not store data. You can operate the pipeline in the same way as a file.
  • A pipe is a stream of bytes. When using a pipe, there is no concept of messages or message boundaries. The process reading data from the pipe can read data blocks of any size, regardless of the size of the data block written by the writing process to the pipe. How many
  • The data passed through the pipe is sequential. The order of bytes read from the pipe is exactly the same as the order in which they were written to the pipe.
  • The direction of data transmission in the pipe is one-way , one end is used for writing, and the other end is used for reading. The pipe is half-duplex.
  • lseek()Reading data from the pipe is a one-time operation. Once the data is read, it is discarded from the pipe, freeing up space for writing more data. You cannot use it to randomly access data in the pipe.
  • Anonymous pipes can only be used between processes with a common ancestor (a parent process and a child process, or two sibling processes, which are related)

Insert image description here


Why pipes can be used for inter-process communication

Or that anonymous pipes can only be used between processes with a common ancestor.
In fact, a pipeline is similar to a file, and the reading and writing of data by the pipeline read and write end is similar to the reading and writing of a file.
Insert image description here


Pipe data structure

The bottom layer is a linear queue, the logic is similar to the circular queue, and the reading and writing order is the same.


Parent and child processes communicate through anonymous pipes

Create an anonymous pipe

int pipe(int pipefd[2]);

  • Function : Create an anonymous pipe for inter-process communication
  • Parameters : int pipefd[2]: This array is an outgoing parameter
    • pipefd[0]: Corresponds to the read end of the pipe
    • pipefd[1]: Corresponds to the write end of the pipeline
  • Return value : 0 if successful; -1 if failed

Notice: Anonymous pipes can only be used for communication between processes with relationships (parent-child processes, sibling processes). If there is no information in the pipeline, the read operation is blocked. If the pipeline is full of information, the write operation is blocked. Reading and writing cannot be performed at the same time, otherwise it will remain blocked. sleep()It cannot be placed read()below otherwise it will cause self-writing and self-reading (commenting out sleep()will also happen).

Code example:
parent-child process that keeps reading and writing

// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    
    

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1)
    {
    
    
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        // 父进程
        printf("i am parent processe, pid = %d\n", getpid());

        char buf[1024] = {
    
    0};
        while (1)
        {
    
    
            // 从管道中读取数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());

            // 向管道中写入数据
            char *str = "hello,i am parent";
            write(pipefd[1], str, strlen(str));
            sleep(1);
        }
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        printf("i am child process, pid = %d\n", getpid());

        char buf[1024] = {
    
    0};
        while (1)
        {
    
    
            // 向管道中写入数据
            char *str = "hello, i am child";
            write(pipefd[1], str, strlen(str));
            sleep(1);

            // 从管道中读取数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("child pecv: %s, pid = %d\n", buf, getpid());
        }
    }

    return 0;
}

Insert image description here


Pipe buffer size

View size

ulimit -a

Insert image description here

Use function to get
long fpathconf(int fd, int name);

  • Function : Used to query file descriptor related attributes
  • Parameters :
    • fd: The file descriptor to be queried
    • name: Indicates the attribute to be queried (constant prefixed with _PC_). The following are commonly used attributes:
      • _PC_LINK_MAX: The maximum number of links to a file
      • _PC_MAX_CANON: The maximum number of bytes in the specification input queue
      • _PC_MAX_INPUT: The maximum number of bytes in the input queue
      • _PC_NAME_MAX: Maximum length of file name
      • _PC_PATH_MAX: The maximum length of the file path
      • _PC_PIPE_BUF: The maximum size of the pipe buffer
      • _PC_CHOWN_RESTRICTED: Indicates whether changing the file owner is allowed
  • Return value : If successful, return the queried attribute value ( longtype); if failed, return -1 and set errno

code example

#include <unistd.h>
#include <stdio.h>

int main(){
    
    
    int pipefd[2];
    int ret = pipe(pipefd);

    // 获取管道的大小
    long size = fpathconf(pipefd[0],  _PC_PIPE_BUF);
    printf("pipe size : %ld\n", size);

    return 0; 
}

Insert image description here

Modify size

ulimit -p

Three situations of anonymous pipe communication

Insert image description here


Problems encountered in the second situation and solutions

Question
Continue the code from the previous chapter. If commented out, sleep()it will directly transform into the first self-writing and self-reading situation:
Insert image description here
Insert image description here

Solution:
rewrite the code into the third case

code example

// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    
    

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1)
    {
    
    
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        // 父进程
        printf("i am parent processe, pid = %d\n", getpid());

        // 关闭写端
        close(pipefd[1]);

        char buf[1024] = {
    
    0};
        while (1)
        {
    
    
            // 从管道中读取数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());
        }
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        printf("i am child process, pid = %d\n", getpid());

        // 关闭读端
        close(pipefd[0]);

        char buf[1024] = {
    
    0};
        while (1)
        {
    
    
            // 向管道中写入数据
            char *str = "hello, i am child\n";
            write(pipefd[1], str, strlen(str));
        }
    }

    return 0;
}

Insert image description here


Anonymous pipe communication case

Implement ps aux | grep xxxparent-child inter-process communication

  • Child process: ps aux, after the child process ends, send the data to the parent process
  • Parent process: obtain data, filter
    pipe(), execlp(), dup2()(the child process redirects standard output to the write end of the pipe)
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>

int main(){
    
    
    // 创建一个管道
    int fd[2];
    int ret = pipe(fd);

    if (ret == -1)
    {
    
    
        perror("pipe");
        exit(0);
    }
    
    // 创建子进程
    pid_t pid = fork();

    if (pid > 0)
    {
    
    
        // 父进程

        // 关闭写端
        close(fd[1]);
        // 从管道中读取
        char buf[4096] = {
    
    0};
        int len = -1;
        // size-1 表示减去字符串结束符 "\0"
        while ((len = read(fd[0], buf, sizeof(buf)-1)) > 0)
        {
    
    
            // 过滤数据输出
            printf("%s", buf);
            memset(buf, 0, 4096);
        }

        // 回收子进程资源
        wait(NULL);

    }else if(pid == 0)
    {
    
    
        // 子进程

        // 关闭读端
        close(fd[0]);

        // 文件描述符的重定向 stdout_fileno -> fd[1]
        dup2(fd[1], STDOUT_FILENO);

        // 执行 ps aux
        int flag = execlp("ps", "ps", "aux", NULL);
        if (flag == -1)
        {
    
    
            perror("execlp");
            exit(0);
        }
    }else
    {
    
    
        perror("fork");
        exit(0);
    }

    return 0;
}

ps: The grep function has not been implemented yet and will be updated later. 4096 is actually not the pipeline memory limit, but data exceeding 4096 will be subcontracted to ensure atomic operations.
If you want to output the actual desired size (< 4096) to the terminal, just leave it len = read(fd[0], buf, sizeof(buf)-1)in a loop.

Pipe read and write characteristics and pipe settings as non-blocking

Pipeline reading and writing features

When using pipes (including anonymous and named), you need to pay attention to the following special situations (assuming they are all blocking I/O operations)

  • All file descriptors pointing to the write end of the pipe are closed (the reference count of the write end of the pipe is 0). If a process reads data from the read end of the pipe, then after the remaining data in the pipe is read, read again will return 0, so Like reading to the end of the file
  • If there is a file descriptor pointing to the write end of the pipe that is not closed (the write end reference count of the pipe is greater than 0), and the process holding the write end of the pipe does not write data to the pipe, and at this time there is a process reading data from the pipe, then the pipe After the remaining data in the pipe is read, read will block again. The data will not be read and returned until there is data in the pipe that can be read.
  • If all the file descriptors pointing to the read end of the pipe are closed (the read end reference count of the pipe is greater than 0), and a process writes data to the pipe at this time, the process will receive a signal SIGPIPE, which usually causes the process to terminate abnormally.
  • If there is a file descriptor pointing to the read end of the pipe that is not closed (the reference count of the read end of the pipe is greater than 0), and the process holding the read end of the pipe does not read data from the pipe, and there is a process writing data to the pipe, then in the pipe When it is full, writing again will block until there is an empty position in the pipe before data can be written and returned.

Summarize

  • Read pipeline:
    • There is data in the pipe: read returns the number of bytes actually read.
    • No data in pipeline:
      • The write end is all closed, and read returns 0 (equivalent to reading to the end of the file)
      • The write end is not completely closed, and read is blocked and waiting.
  • Write pipe:
    • All pipe readers are closed and the process terminates abnormally (the process receives the SIGPIPE signal)
    • The pipe readers are not all closed:
      • Pipe is full, write blocks
      • The pipe is not full, write writes the data and returns the actual number of bytes written

Pipe set to non-blocking

code example

// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
/*
    设置管道非阻塞
    fcntl
*/ 

int main()
{
    
    

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1)
    {
    
    
        perror("pipe");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        // 父进程
        printf("i am parent processe, pid = %d \n", getpid());

        // 关闭写端
        close(pipefd[1]);

        char buf[1024] = {
    
    0};

        // 获取原来的flag
        int flags = fcntl(pipefd[0], F_GETFL); 

        // 修改flag的值
        flags |= O_NONBLOCK;
        
        // 设置新的flag
        fcntl(pipefd[0], F_SETFL,  flags);
        
        while (1)
        {
    
    
            // 从管道中读取数据
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("len=%d \n", len);
            printf("parent recv : %s , pid : %d \n", buf, getpid());
            memset(buf, 0, 1024);
            sleep(1);
        }
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        printf("i am child process, pid = %d \n", getpid());

        // 关闭读端
        close(pipefd[0]);

        char buf[1024] = {
    
    0};
        while (1)
        {
    
    
            // 向管道中写入数据
            char *str = "hello, i am child \n";
            write(pipefd[1], str, strlen(str));
            sleep(3);
        }
    }

    return 0;
}

Insert image description here


famous channel

A named pipe (FIFO), also known as a FIFO file, provides a path name associated with it. As long as the path can be accessed, they can communicate with each other through the FIFO .
A named pipe is opened in the same way as an ordinary file. Once opened, the same I/O system functions as those used to operate anonymous pipes and other files can be used on it.

The difference between named pipes and anonymous pipes

  • FIFO exists as a special file in the file system, but the contents of the FIFO are stored in memory
  • After the process using FIFO exits, the EIFO file continues to be saved in the file system for later use
  • FIFOs have names, and unrelated processes can communicate by opening named pipes.

Use of famous pipes

Command creation
Once a FIFO has been created using mkfifo, open()it can be opened using . Common file I/0 functions can be used for FIFO. Such as: close(), read(), write(), unlink()etc.

mkfifo filename

function creation
int mkfifo(const char *pathname, mode t mode);

  • Function : Create FIFO file
  • Parameters :
    • pathname: path to pipe name
    • mode: File permissions (same as open()of mode)

code example

write.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
//  向管道中写数据

int main(){
    
    

    // 1. 判断文件是否存在
    int flag = access("fifo_test", F_OK);
    if (flag == -1)
    {
    
    
        printf("管道不存在!请创建管道\n");

        // 1.1 不存在则创建管道文件
        int ret = mkfifo("fifo_test", 0664);
        
        if (ret == -1)
        {
    
    
            perror("mkfifo");
            exit(0);
        }
    }

    // 2. 以只写方式打开管道
    int fifo_fd = open("fifo_test", O_WRONLY);
    if (fifo_fd == -1)
    {
    
    
        perror("open");
        exit(0);
    }

    // 3. 写数据
    for (int i = 0; i < 100; i++)
    {
    
    
        char buf[1024];
        sprintf(buf,"hello,%d\n", i);
        printf("write data:%s\n", buf);
        write(fifo_fd, buf, strlen(buf));
        sleep(1);
    }

    // 4. 关闭FIFO文件描述符
    close(fifo_fd);

    return 0;
}

read.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
//  从管道中读数据

int main(){
    
    
    // 1. 打开管道文件
    int fifo_fd = open("fifo_test", O_RDONLY);
    if(fifo_fd == -1){
    
    
        perror("open");
        exit(0);
    }

    // 2. 读取数据
    while (1)
    {
    
    
        char buf[1024] = {
    
    0};
        int len = read(fifo_fd, buf, sizeof(buf));
        if (len == 0)
        {
    
    
            printf("写端已断开连接...\n");
            break;
        }
        printf("recv buf:%s\n", buf);
    }
    
    // 3. 关闭文件描述符
    close(fifo_fd);

    return 0;
}

two processes running
Insert image description here
Insert image description here

Notice

  • A process that opens a pipe for reading only blocks until another process opens the pipe for writing only.
  • A process that opens a pipe for writing only blocks until another process opens the pipe for reading only.

Famous channels implement simple version of chat function

basic framework

Insert image description here

Write, read and chat

Code example
chatA.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main(){
    
    
    // 1.判断有名管道是否存在
    int ret = access("fifo1", F_OK);
    if (ret == -1)
    {
    
    
        // 文件不存在
        printf("管道文件不存在,创建对应有名管道\n");
        ret = mkfifo("fifo1", 0664);
        if (ret == -1)
        {
    
    
            perror("mkfifo");
            exit(0);
        }
    }

    // 2.以只写的方式打开管道fifo1
    int w_fd = open("fifo1", O_WRONLY);
    if (w_fd == -1)
    {
    
    
        perror("open");
        exit(0);
    }
    printf("管道fifo1打开成功,等待写入...\n");

    // 3.以只读的方式打开管道fifo2
    int r_fd = open("fifo2", O_RDONLY);
    if (r_fd == -1)
    {
    
    
        perror("open");
        exit(0);
    }
    printf("管道fifo2打开成功,等待读取...\n");

    char buf[128];
    // 4.循环地写读数据
    while(1)
    {
    
    
        memset(buf, 0, 128);
        // 4.1.获取标准输入的数据
        fgets(buf, 128, stdin);
        // 4.2.写数据
        ret = write(w_fd, buf, strlen(buf));
        if (ret == -1)
        {
    
    
            perror("write");
            exit(0);
        }
        
        // 4.3.读数据
        memset(buf, 0, 128);
        ret = read(r_fd, buf, 128);
        if (ret == -1)
        {
    
    
            if (ret <= 0 )
            {
    
    
                perror("read");
                break;
            }
        }
        printf("chatB: %s\n",buf);
    }

    // 5.关闭读写文件描述符
        close(r_fd);
        close(w_fd);

    return 0;
}

catB.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main(){
    
    
    // 1.判断有名管道是否存在
    int ret = access("fifo2", F_OK);
    if (ret == -1)
    {
    
    
        // 文件不存在
        printf("管道文件不存在,创建对应有名管道\n");
        ret = mkfifo("fifo2", 0664);
        if (ret == -1)
        {
    
    
            perror("mkfifo");
            exit(0);
        }
        
    }

    // 2.以只读的方式打开管道fifo1
    int r_fd = open("fifo1", O_RDONLY);
    if (r_fd == -1)
    {
    
    
        perror("open");
        exit(0);
    }
    printf("管道fifo1打开成功,等待读取...\n");

    // 3.以只写的方式打开管道fifo2
    int w_fd = open("fifo2", O_WRONLY);
    if (w_fd == -1)
    {
    
    
        perror("open");
        exit(0);
    }
    printf("管道fifo2打开成功,等待写入...\n");

    char buf[128];

    // 4.循环地读写数据
    while(1)
    {
    
    
        // 4.1.读数据
        memset(buf, 0, 128);
        ret = read(r_fd, buf, 128);
        if (ret == -1)
        {
    
    
            if (ret <= 0 )
            {
    
    
                perror("read");
                break;
            } 
        }
        printf("chatA: %s\n",buf);

        memset(buf, 0, 128);

        // 4.1.获取标准输入的数据
        fgets(buf, 128, stdin);

        // 4.2.写数据
        ret = write(w_fd, buf, strlen(buf));
        if (ret == -1)
        {
    
    
            perror("write");
            exit(0);
        }
    }

    // 5.关闭读写文件描述符
    close(w_fd);
    close(r_fd);
    
    return 0;
}

Insert image description here
Insert image description here


memory map

What is memory mapping

Memory-mapped I/O maps disk file data to memory, and users can modify the disk file by modifying the memory .

Insert image description here


Memory mapping related system calls

void *mmap(void *addr, size t length, int prot, int flags, int fd, off_t offset);

  • Function : Map the data of a file or device into memory

  • Parameters :

    • addr: NULL, specified by the kernel
    • length: The length of the data to be mapped. This value cannot be 0. It is recommended to use the length of the file to obtain the length of the file: stat(),lseek()
    • prot: Operation permission for the applied memory mapping area. To operate the mapped memory, you must have read permission (commonly used: PROT_READ, PROT_READ | PROT_WRITE)
      • PROT_EXEC: Executable permissions
      • PROT_READ:Read permission
      • PROT_WRITE:write permission
      • PROT_NONE:permission denied
    • flags
      • MAP_SHARED: The data in the mapping area will be automatically synchronized with the disk file. This option must be set for inter-process communication.
      • MAP_PRIVATE: Not synchronized, the data in the memory mapping area has changed, the original file will not be modified, and a new file will be re-created (copy-on-write)
    • fd: The file descriptor of the file that needs to be mapped
      • Obtained through open, which is a disk file.
      • Notice: The size of the file cannot be 0, and the permissions specified by open cannot protconflict with
        prot: PROT_READcorresponds to open: read-only/read-write
        prot: PROT_READ | PROT_WRITEcorresponds to open: read-write
    • offset: Offset, generally not used. What must be specified is an integer multiple of 4096, 0 means no offset.
  • Return value : If successful, return the first address of the created memory; if failed, return MAP_FAILED( (void*) -1 ) and set errno


int munmap(void *addr, size t length);

  • Function : Release memory mapping
  • Parameters :
    • addr: The first address of the memory to be released
    • length: The length of the data to be released (memory size), this value cannot be 0. It is recommended to use the length of the file to obtain the length of the file: stat(),lseek()
  • Return value : Returns 0 if successful; returns -1 if failed, and sets errno

Using memory mapping for inter-process communication

Implementation principle

  • Relational inter-process communication (parent-child process)
    • When there is no child process, first create a memory mapping area through the only parent process.
    • After having the memory mapping area, create a child process
    • The memory mapping area created by the parent and child processes is shared
  • Inter-process communication without relationships
    • Prepare a disk file with a size other than 0
    • 进程1: Create a memory mapping area through a disk file and get a pointer to operate this memory.
    • 进程2: Create a memory mapping area through a disk file and get a pointer to operate this memory.
    • Use memory mapped area communication

Notice: Memory mapped area communication is non-blocking

Code Example
Relational Inter-Process Communication

#include <stdio.h>
#include <wait.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    
    
    // 1.打开一个文件
    int fd = open("test.txt", O_RDWR);
    int size = lseek(fd, 0, SEEK_END); // 获取文件大小

    // 2.创建内存映射
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED)
    {
    
    
        perror("mmap");
        exit(0);
    }

    // 3.创建子进程
    pid_t pid = fork();
    if (pid > 0)
    {
    
    
        wait(NULL);
        // 父进程
        char buf[64];
        strcpy(buf, (char *)ptr);
        printf("read data : %s\n", buf);
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        strcpy((char *)ptr, "nihao, this's son!!!");
    }
    else
    {
    
    
        perror("fork");
        exit(0);
    }

    // 4.释放内存映射区
    munmap(ptr, size);

    return 0;
}

Insert image description here


Memory mapping related issues

1. If the ++ operation ( ) is performed on mmap()the return value of , will it be successful? ptrptr++munmap()
Answer : The ++ operation can be performed, but it cannot be released successfully. The first address must be passed.

2. What will happen if open()when is O RDONLYspecified ? mmap()protPROT_READ | PROT_WRITE
Answer : An error will be returned MAP_FAILED. It is recommended thatopen() the permissions and protpermissions be consistent.

3. What happens if the file offset is 1000?
Answer : The offset must be an integer multiple of 4096, returnMAP_FAILED

4. mmap()Under what circumstances will the call fail?
Answer : ① length= 0; ② protOnly write permission is specified; ③ The second question above occurs

5. Is it possible open()to O_CREATcreate a mapping area with a new file?
Answer : Yes, but the size of the created file is not 0 (it can be expanded using lseek()and ).truncate()

6. mmap()After closing the file descriptor, mmap()will it have any impact on the mapping?
Answer : No impact, the mapping area still exists, it is just that the mapping area fdwas closed.

ptr7. What will happen if an out-of-bounds operation occurs?
Answer : An out-of-bounds operation operates on illegal memory and will generate a segmentation fault.


Implement file copy function through memory mapping

Thought steps

  1. Memory map the original file
  2. Create a new file (extend the file)
  3. Map the data of the new file into memory
  4. Copy the memory data of the first file to the new file memory through memory copy
  5. Release resources

code example

#include <stdio.h>
#include <wait.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>


int main(){
    
    
    // 1.对原始的文件进行内存映射
    int fd = open("english.txt", O_RDWR);
    if (fd == -1)
    {
    
    
        perror("open");
        exit(0);
    }

    // 获取源文件大小
    int len = lseek(fd, 0, SEEK_END);

    // 2.创建一个新文件(拓展该文件)
    int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0644);
    if (fd1 == -1)
    {
    
    
        perror("open");
        exit(0);
    }

    // 对新建文件进行拓展
    truncate("cpy.txt", len);
    write(fd1, " ", 1);

    // 3.把新文件的数据映射到内存中
    void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 
    void *ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);   
    
    if(ptr == MAP_FAILED)
    {
    
    
        perror("mmap");
        exit(0);
    }
    if(ptr1 == MAP_FAILED)
    {
    
    
        perror("mmap");
        exit(0);
    }

    //内存拷贝
    memcpy(ptr1, ptr, len);

    // 释放资源(谁先打开先释放)
    munmap(ptr1, len);
    munmap(ptr, len);

    // 关闭文件描述符
    close(fd1);
    close(fd);

    return 0;
}

Notice: Generally speaking, it is inconvenient to copy files that are too large to prevent insufficient memory.

Anonymous mapping

The file entity process does not require a memory mapping .
You can do related process (parent-child process) mapping.

code example

#define _DEFAULT_SOURCE // MAP_ANONYMOUS

#include <stdio.h>
#include <wait.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

#define BUF_LEN 4096

int main()
{
    
    

    // 1.创建匿名映射区
    void *ptr = mmap(NULL, BUF_LEN, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (ptr == MAP_FAILED)
    {
    
    
        perror("mmap");
        exit(0);
    }

    // 2.父子间通信
    pid_t pid = fork();

    if (pid > 0)
    {
    
    
        // 父进程
        strcpy((char *)ptr, "hello, world");
        wait(NULL);
    }
    else if (pid == 0)
    {
    
    
        // 子进程
        sleep(1);
        printf("%s\n", (char *)ptr);
    }

    // 3.释放内存映射区
    int ret = munmap(ptr, BUF_LEN);
    if (ret == -1)
    {
    
    
        perror("munmap");
        exit(0);
    }

    return 0;
}

Insert image description here


Signal

what is signal

Signals are Linux inter-process communication, which are notification mechanisms for processes when events occur, sometimes also called software interrupts .
Signals are a simulation of the interrupt mechanism at the software level and a method of asynchronous communication.
Signals can cause a running process to be interrupted by another running asynchronous process and instead handle an unexpected event.

Many signals sent to the process usually originate from the kernel . The various events that cause the kernel to generate signals for processes are as follows:

  • For a foreground process, the user can send a signal to it by entering special terminal characters. For example, typing ctrl+C usually sends an interrupt signal to the process. For example, typing ctrl+C Ctrl+Cusually sends an interrupt signal to the process.

  • A hardware exception occurs, that is, the hardware detects an error condition and notifies the kernel, which then sends the corresponding signal to the relevant process. For example, executing an abnormal machine language instruction, such as dividing by 0, or referencing an inaccessible memory area.

  • System status changes. For example, alarm()the expiration of the timer will cause SIGALRMa signal, the CPU time of the process execution exceeds the limit, or a child process of the process exits.

  • Run the kill command or callkill()


Two purposes of using signals

  • Let a process know that a specific thing has occurred

  • Forces a process to execute a signal handler in its own code


Signal characteristics

  • Simple

  • Can’t carry a lot of information

  • Send only when certain conditions are met

  • Priority is higher


View a list of system-defined signals

kill -l

ps: The first 31 signals are regular signals, and the rest are real-time signals


Linux signal list

serial number Signal name Corresponding event Default action
1 SIGHUP All processes started by the shell when the user exits the shell will receive this signal Terminate process
2 SIGINT
When the user presses Ctrl+Cthe key combination, the user terminal sends this signal to the running program started by the terminal. Terminate process
3 FOLLOWS
This signal is generated when the user presses Ctrl+\the key combination, and the user terminal sends some signals to the running program started by the terminal. Terminate process
4 SIGIL The CPU detects that a process has executed an illegal instruction Terminate the process and generate the core file
5 SIG TRAP This signal is generated by breakpoint instructions or other trap instructions Terminate the process and generate the core file
6 SIGABRT abort()This signal is generated when calling Terminate the process and generate the core file
7 SIGBUS Illegal access to memory address, including memory alignment error Terminate the process and generate the core file
8 SIGFPE Issued when a fatal arithmetic error occurs. Including not only floating point operation errors, but also all algorithm errors such as overflow and division by zero. Terminate the process and generate the core file
9 SIGKILL
Terminate the process unconditionally. This signal cannot be ignored, handled and blocked Terminate the process, which can kill any normal process (abnormal processes such as zombie processes are not counted)
10 1 User-defined signals. That is, the programmer can define and use the signal in the program Terminate process
11 SIGSEGV
Indicates that the process made an invalid memory access (segmentation fault) Terminate the process and generate the core file
12 SIGUSR2 Another user-defined signal that programmers can define and use in the program Terminate process
13 SIGPIPE
Broken pipe writes data to a pipe without a read end Terminate process
14 SIGALRM The timer times out, and the timeout time alarm()is set by the system call Terminate process
15 TARGET TERM Program end signal. Unlike SIGKILL, this signal can be blocked and terminated. Usually used to indicate that the program exits normally. This signal is generated by default when executing the shell command Kill. Terminate process
16 SIGSTKFLT Signals that appeared in earlier versions of Linux are still backwards compatible Terminate process
17 SIGCHLD
When the child process ends, the parent process will receive this signal ignore this signal
18 SIGCONT
If the process is stopped, keep it running continue/ignore
19 SIGSTOP
Stop the execution of the process. Signals cannot be ignored, handled and blocked To terminate the process
20 SIGTSTP Stop the terminal interactive process from running. This signal is emitted when the <ctrl+z> key combination is pressed pause process
21 SIGTTIN Background process reads terminal console pause process
22 SIGTTOU This signal is similar to SIGTTIN and occurs when the background process wants to output data to the terminal. pause process
23 SIGURG When there is urgent data on the socket, some signals are sent to the currently running process to report the arrival of urgent data. If network out-of-band data arrives ignore this signal
24 SIGXCPU When the process execution time exceeds the CPU time allocated to the process, the system generates this signal and sends it to the process. Terminate process
25 SIGXFSZ Maximum file length setting exceeded Terminate process
26 SIGVTALRM This signal is generated when the virtual timer times out. Similar to SIGALRM, but this signal only counts the CPU time occupied by the process Terminate process
27 SGI PROF Similar to SIGVTALRM, it not only includes the CPU time occupied by the process but also includes the execution time of system calls. Terminate process
28 SIGWINCH Emitted when the window changes size ignore this signal
29 SIGIO This signal indicates to the process that an asynchronous IO event has been issued ignore this signal
30 SINGAPORE Shut down ignore this signal
31 SIGSYS Invalid system call Terminate the process and generate the core file
34
~
64
SIGRTMIN
~
SIGRTMAX
LINUX real-time signals, they have no fixed meaning (can be customized by the user) Terminate process

ps: Signals marked in red need to be mastered. SIGKILL and SIGSTOP signals cannot be caught, blocked or ignored, and only default actions can be performed .


5 default processing actions for signals

View signal details

man 7 signal

5 default processing actions for signals

  • Term : terminate the process
  • Ign : The current process ignores this signal
  • Core : terminate the process and generate a core file
  • Stop : suspend the current process
  • Cont : Continue execution of the currently suspended process

ps:
Core file saves information about abnormal exit of the process

#include <stdio.h>
#include <string.h>
int main() (
	// 没有指向合法内存
	char * buf;
	strcpy(buf,"hello");
	return 0;

Segmentation error when compiling and generating target files

gcc core.c
./a.out

Insert image description here
Generate Core file

ulimit -a
ulimit -c 1024
./a.out

Insert image description here
Insert image description here
Insert image description here
Debugging Core files

gdb a.out
# 进入GDB调试界面
(gdb) core-file core

Insert image description here
Insert image description here

Several states of signals

  • produce
  • pending
  • delivery

kill, raise, abort functions

int kill(pid t pid, int sig);

  • 作用:给任何的进程或者进程组 pid,发送任何的信号 sig

  • 参数

    • pid
      • > 0:将信号发送给指定的进程
      • = 0:将信号发送给当前的进程组
      • = -1:将信号发送给每一个有权限接收这个信号的进程
      • <-1:这个pid=某个进程组的ID取反 (如:-12345)
    • sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号
  • 返回值:如果成功返回0;如果失败返回-1,并设置errno


int raise(int sig);

  • 作用:给当前进程发送信号

  • 参数sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号

  • 返回值:如果成功返回0;如果失败返回-1,并设置errno


void abort(void);

  • 作用:发送SIGABRT信号给当前的进程,杀死当前进程

代码示例

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main(){
    
    

    pid_t pid = fork();

    if (pid == 0)
    {
    
    
        //子进程
        for (int i = 0; i < 5; i++)
        {
    
    
            printf("child process\n");
            sleep(1);
        }
    }else if (pid > 0)
        {
    
    
            // 父进程
            printf("parent process\n");
            sleep(2);
            printf("kill child process now\n");
            kill(pid, SIGINT);
        }
    

    return 0;
}

Insert image description here


alarm 函数

unsigned int alarm(unsigned int seconds);

  • 作用:设置定时器(闹钟) 。函数调用,开始倒计时,当倒计时为8的时候函数会给当前的进程发送一个信号: SIGALARM
  • 参数seconds:倒计时的时长,单位: 秒。如果参数为0,定时器无效(不进行倒计时,不发信号),如取消一个定时器:alam(0)
  • 返回值:如果之前没有计时器,返回0;如果之前有计时器返回之前计时器剩余的时间

SIGALARM : 默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
例子

alarm(10); //-> 返回0
//过了1秒
alarm(5); //-> 返回9

alam(100); //该函数不阻塞

代码示例

#include <stdio.h>
#include <unistd.h>
int main() {
    
    
    int seconds = alarm(5);
    printf("seconds = %d\n", seconds); // 0

    sleep(2);
    seconds = alarm(2); //不阻塞
    printf("seconds = %d\n", seconds);// 3
    //while(1){}第一个例子

	alarm(0);
    
    // 1s电脑能数多少数//第二个例子
    alarm(1);
    int i = 0;
    while (1)
    {
    
    
        printf("%i\n", i++);
    }

    return 0;
}

Insert image description here
Insert image description here
Insert image description here

ps:其实1s内计算机真实计数远远大于 2.txt 中的数。实际的时间 = 内核时间 + 用户时间 + 消耗的时间。进行文件IO操作的时候比较浪费时间。定时器与进程的状态无关(自然定时法),无论进程处于什么状态,alarm() 都会计时。


setitimer 定时器函数

int setitimer(int which, const struct itimerval *new_ value, struct itimerval *old value);

  • 作用:设置定时器(闹钟)。可以替代alarm() ,精度微秒:us,可以实现周期性定时
  • 参数
    • which:定时器以什么时间计时
      • ITIMER_REAL:真实时间,时间到达,发送 SIGALRM(常用)
      • ITIMER_VIRTUAL:用户时间,时间到达,发送 SIGVTALRM
      • ITIMER_PROF:以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送
    • new_value:设置定时器的属性
    • old_value:记录上一次定时器的时间参数,一般不用,指定NULL
  • 返回值:如果成功返回0;如果失败返回-1,并设置errno
// 定时器的结构体
struct itimerval {
    
    
	// 每个阶段的时间,间隔时间
	struct timeval it interval;
	
	// 延迟多长时间执行定时器
	struct timeval it_value;
};

// 时间的结构体
struct timeval {
    
    
	// 秒数
	time_ttv_sec;
	
	// 微秒
	suseconds t tv_usec;
};

代码示例
过3秒以后,每隔2秒钟定时一次

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>

int main(){
    
    

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间,3秒后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value .tv_usec = 0;
    int ret =setitimer(ITIMER_REAL, &new_value, NULL);// 非阻塞
    printf("定时器开始了...\n");
    if(ret == -1)
    {
    
    
        perror("setitimer");
        exit(0);    
    }

    getchar();
    return 0;
}

Insert image description here


signal 信号捕捉函数

sighandler_t signal(int signum, sighandler_t handler);

  • 作用:设置某个信号的捕捉行为
  • 参数
    • signum:要捕捉的信号
    • handler:捕捉到信号要如何处理
      • SIG_IGN:忽略信号
      • SIG_DFL:使用信号默认的行为
      • 回调函数:这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号
  • 返回值:如果成功返回上一次注册的信号处理函数的地址,第一次调用返回NULL;如果失败返回SIG_ERR,设置errno

ps:SIGKILL、SIGSTOP不能被捕捉,不能被忽略。
回调函数需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义,不是程序员调用,而是当信号产生,由内核调用。函数指针是实现回调的函数实现之后,将函数名放到函数指针的位置就可以了。

代码示例

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num)
{
    
    
    printf("捕捉到了信号的编号是: %d\n", num);
    printf("xxxxxxx\n");
}

int main(){
    
    

    // 注册信号捕捉
    // signal(SIGALRM,SIG_IGN);
    // signa1(SIGALRM,SIG_DFL);
    // void (*sighandler_t)(int); 函数指针,int 类型的参数表示信号捕捉到的值

    __sighandler_t flag = signal(SIGALRM, myalarm);
    if (flag == SIG_ERR)
    {
    
    
        perror("signal");
        exit(0);
    }
    

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间
    new_value.it_value.tv_sec = 3;
    new_value.it_value .tv_usec = 0;
    int ret =setitimer(ITIMER_REAL, &new_value, NULL);// 非阻塞
    printf("定时器开始了...\n");
    if(ret == -1)
    {
    
    
        perror("setitimer");
        exit(0);    
    }

    getchar();
    return 0;
}

Insert image description here

信号集及相关函数

什么是信号集

许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t
在 PCB 中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改


阻塞信号集和未决信号集

信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间

信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

工作原理
Insert image description here

  1. 用户通过键盘ctrl + C,产生2号信号SIGINT (信号被创建)

  2. 信号产生但是没有被处理 (未决)

    • 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
    • SIGINT信号状态被存储在第二个标志位上
      • 这个标志位的值为0,说明信号不是未决状态
      • 这个标志位的值为1,说明信号处于未决状态
  3. 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较

    • 阻塞信号集默认不阻塞任何的信号
    • 如果想要阻塞某些信号需要用户调用系统的API
  4. 在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了

    • 如果没有阻塞,这个信号就被处理
    • 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理

信号集相关函数

int sigemptyset(sigset_t *set);

  • 作用:清空信号集中的数据,将信号集中的所有的标志位置为0

  • 参数set:传出参数,需要操作的信号集

  • 返回值:成功返回0,失败返回-1


int sigfillset(sigset t *set);int sigaddset(sigset_t *set, int signum);

  • 作用:将信号集中所有的标志位设为1

  • 参数

    • set:传出参数,需要操作的信号集
    • signum: 需要设置阻塞的那个信号
  • 返回值:成功返回0,失败返回-1


int sigaddset(sigset_t *set, int signum);

  • 作用:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号

  • 参数

    • set:传出参数,需要操作的信号集
    • signum: 需要设置阻塞的那个信号
  • 返回值:成功返回0;失败返回-1


int sigdelset(sigset t *set, int signum);

  • 作用:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号

  • 参数set:传出参数,需要操作的信号集

  • 返回值:成功返回0;失败返回-1


int sigismember(const sigset_t *set, int signum);

  • 作用:判断某个信号是否阻塞

  • 参数

    • set:需要操作的信号集
    • signum: 需要判断的那个信号
  • 返回值:如果成功返回1表示 signum被阻塞,返回0表示signum不阻塞;如果失败返回-1


代码示例

#define _DEFAULT_SOURCE
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

int main(){
    
    

    // 创建一个信号集
    sigset_t set;

    // 清空信号集的内容
    sigemptyset(&set);

    // 判断 SIGINT 是否在信号集 set 里
    int ret = sigismember(&set, SIGINT);
    if (ret == 0)
    {
    
    
        printf("SIGINT 不阻塞\n");
    }else if (ret == 1)
    {
    
    
        printf("SIGINT 阻塞\n");
    }
    
    // 添加几个信号到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 判断SIGINT是否在信号集中
    ret = sigismember(&set, SIGINT);
    if(ret == 0) {
    
    
        printf("SIGINT 不阻塞\n");
    } else if(ret == 1) {
    
    
        printf("SIGINT 阻塞\n");
    }

    // 判断SIGQUIT是否在信号集中
    int jug = sigismember(&set, SIGQUIT);
    if(jug == 0){
    
    
        printf("SIGQUIT 不阻塞\n");
    } else if(ret == 1){
    
    
        printf("SIGQUIT 阻塞\n");
    }

    // 从信号集中删除一个信号
    sigdelset(&set, SIGQUIT);

    // 判断SIGQUIT是否在信号集中
    jug = sigismember(&set, SIGQUIT);
    if(jug == 0){
    
    
        printf("SIGQUIT 不阻塞\n");
    } else if(ret == 1){
    
    
        printf("SIGQUIT 阻塞\n");
    }


    return 0;
}

Insert image description here


sigprocmask 函数使用

int sigprocmask(int how,const sigset_t *set, sigset_t *oldset);

  • 作用:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)

  • 参数

    • how:如何对内核阻塞信号集进行处理
      • SIG_BLOCK:将用户设置的阻塞信号集添加到内核中,内核中原来的数据
        假设内核中默认的阻塞信号集是maskmask | set
      • SIG_UNBLOCK:根据用户设置的数据,对内核中的数据进行解除阻塞
        mask &= ~set
        SIG_SETMASK:覆盖内核中原来的值
    • set:已经初始化好的用户自定义的信号集
    • oldset:保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
  • 返回值:如果成功返回0;如果失败返回-1,并设置错误号EFAULTEINVAL


int sigpending(sigset_t *set);

  • 作用:获取内核中的未决信号集

  • 参数set:传出参数,保存的是内核中的未决信号集中的信息。

  • 返回值:如果成功返回0;如果失败返回-1,并设置errno


代码示例
编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕设置某些信号是阻塞的,通过键盘产生这些信号

#define _DEFAULT_SOURCE
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    
    
    // 设置2、3号信号阻塞
    sigset_t set;
    sigemptyset(&set);

    // 将2号和3号信号添加到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 修改内核中的阻塞信号集
    sigprocmask(SIG_BLOCK, &set, NULL);

    int num = 0;
    while (1)
    {
    
    
        num++;
        // 获取当前的未决信号集的数据
        sigset_t pendingset;
        sigemptyset(&pendingset);
        sigpending(&pendingset);

        // 遍历前32位
        for (int i = 1; i <= 32; i++)
        {
    
    
            if (sigismember(&pendingset, i) == 1)
            {
    
    
                printf("1");
            }
            else if (sigismember(&pendingset, i) == 0)
            {
    
    
                printf("0");
            }
            else
            {
    
    
                perror("sigismember");
                exit(0);
            }
        }
        printf("\n");
        sleep(1);
        if (num == 10)
        {
    
    
            // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }
    }

    return 0;
}

Insert image description here


sigaction 信号捕捉函数

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

  • 作用:检查或者改变信号的处理。信号捕捉

  • 参数

    • signum:需要捕捉的信号的编号或者宏值(信号的名称)
    • act:捕捉到信号之后的处理动作
    • oldact:上一次对信号捕捉相关的设置,一般不使用,传递NULL
  • 返回值:如果成功返回0;如果失败返回-1,并设置errno


sigaction 结构体

struct sigaction {
    
    
	//函数指针,指向的函数就是信号捕捉到之后的处理函数
	void (*sa_handler)(int);
	
	//不常用
	void (*sa_sigaction)(int, siginfo_t*, void *);
	
	//临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
	sigset_t sa_mask;
	
	//使用哪一个信号处理对捕捉到的信号进行处理
	
	//这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示sa_sigaction
	int sa_flags;
	// 已废弃
	void (*sa_restorer)(void);

代码示例

#define _DEFAULT_SOURCE
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void myalarm(int num)
{
    
    
    printf("捕捉到了信号的编号是: %d\n", num);
    printf("xxxxxxx\n");
}

int main(){
    
    

    // 注册信号捕捉
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = myalarm;
    // 清空临时阻塞信号集
    sigemptyset(&act.sa_mask);

    sigaction(SIGALRM, &act, NULL);


    __sighandler_t flag = signal(SIGALRM, myalarm);
    if (flag == SIG_ERR)
    {
    
    
        perror("signal");
        exit(0);
    }
    

    struct itimerval new_value;

    // 设置间隔的时间
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    // 设置延迟的时间
    new_value.it_value.tv_sec = 3;
    new_value.it_value .tv_usec = 0;
    int ret =setitimer(ITIMER_REAL, &new_value, NULL);// 非阻塞
    printf("定时器开始了...\n");
    if(ret == -1)
    {
    
    
        perror("setitimer");
        exit(0);    
    }

    getchar();
    return 0;
}

Insert image description here


内核实现信号捕捉的过程

Insert image description here


SIGCHLD 信号

SIGCHLD信号产生的条件

  • 子进程终止时
  • 子进程接收到SIGSTOP信号暂停时
  • 子进程处在停止态,接受到SIGCONT后唤醒时(继续运行)

ps:以上三种条件都会给父进程发送SIGCHLD信号,父进程默认会忽略该信号。可以使用SIGCHLD信号解决僵尸进程的问题。

代码示例

#define _DEFAULT_SOURCE
#include <stdio.h> 
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>

void myFun(int num)
{
    
    
    printf("捕捉到的信号:%d\n", num); // 回收子进程PCB的资源
    // while(1) {
    
    
    // wait(NULL);
    // }
    while (1)
    {
    
    
        int ret = waitpid(-1, NULL, WNOHANG);
        if (ret > 0)
        {
    
    
            printf("child die , pid = %d\n", ret);
        }
        else if (ret == 0)
        {
    
    
            // 说明还有子进程或者
            break;
        }else if (ret == -1)
        {
    
    
            break;
        }
        
    }
}

int main() {
    
    

    // 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号符
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, NULL);

    //创建一些子进程
    pid_t pid;
    for(int i = 0; i < 20; i++) {
    
    
        pid = fork();
        if(pid == 0) {
    
    
            break;
        }
    }

    if(pid > 0) {
    
    
        //父进程

        //捕捉子进程死亡时发送的SIGCHLD信号
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFun;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD, &act, NULL);

        //注册完信号捕捉以后,解除阻塞
        sigprocmask(SIG_UNBLOCK, &set, NULL);

        while(1){
    
    
            printf("parent process pid : %d\n", getpid());
            sleep(2);
        }
    }
    else if(pid == 0){
    
    
        //子进程
        printf("child process pid : %d\n", getpid());
    }
	return 0;
}

Insert image description here


共享内存

什么是共享内存

共享内存是一种用于多进程或多线程之间共享数据的机制,它允许不同的进程或线程在物理内存创建一个共享区域(段)。
由于一个共享内存段会称为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。


共享内存使用步骤

  1. 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
  2. 使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
  3. 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存程序需要使用由 shmat() 调用返回的 addr,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
  4. 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
  5. 调用shmctl()来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存才会销毁。只有—个进程需要执行这一步

共享内存相关函数

int shmget(key_t key, size_t size, int shmflg);

  • 作用:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识
    新创建的内存段中的数据都会被初始化为0

  • 参数

    • keykey_t 类型是一个整形,通过这个找到或者创建一个共享内存。
      一般使用16进制表示,非0值
    • size:共享内存的大小
    • shmflg:属性
      • 访问权限
      • 附加属性:创建/判断共享内存是不是存在
        • 创建:IPC_CREAT
        • 判断共享内存是否存在:IPC_EXCL,需要和 IPC_CREAT 一起使用 IPC_CREAT | IPC_EXCL | 0664
  • 返回值:如果成功>0返回共享内存的引用的ID(后面操作共享内存都是通过这个值);如果失败返回-1,并设置errno


void *shmat(int shmid, const void *shmaddr, int shmflg);

  • 作用:和当前的进程进行关联-参数

  • 参数

    • shmid:- shmid :共享内存的标识(ID),由shmget 返回值获取
    • shmaddr:申请的共享内存的起始地址,指定NULL,内核指定
    • shmflg:对共享内存的操作
      • 读:SHM_RDONLY,必须要有读权限
      • 读写:0
  • 返回值:如果成功>0返回共享内存的起始地址;如果失败返回(void*)-1,并设置errno
    int shmdt(const void *shmaddr);

  • 作用:解除当前进程和共享内存的关联

  • 参数shmaddr:共享内存的首地址

  • 返回值:如果成功返回0;如果失败返回-1


int shmctl(int shmid, int cmd, struct shmid_ds *buf);

  • 作用:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共
  • 参数
    • shmid:共享内存的ID
    • cmd:要做的操作
      • IPC_STAT:获取共享内存的当前的状态
      • IPC_SET:设置共享内存的状态
      • IPC_RMID:标记共享内存被销毁
    • buf:需要设置或者获取的共享内存的属性信息
      • IPC_STAT:buf存储数据
      • IPC_SET:buf中需要初始化数据,设置到内核中
    • IPC RMID:没有用,NULL
  • 返回值:如果成功返回0;如果失败返回-1

代码示例
write_shm.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

int main()
{
    
    

    // 1.创建一个共享内存
    int shmid = shmget(100, 4096, IPC_CREAT | 0664);
    
    // 2.和当前进程进行关联
    void *ptr = shmat(shmid, NULL, 0);

    char *str = "helloworld";

    // 3.写数据
    memcpy(ptr, str, strlen(str) + 1);
    printf("按任意键继续\n");
    getchar();

    // 4.解除关联
    shmdt(ptr);

    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

read_shm.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main()
{
    
    

    // 1.获取一个共享内存
    int shmid = shmget(100, 0, IPC_CREAT);
    printf("shmid: %d\n", shmid);

    // 2.和当前进程进行关联
    void *ptr = shmat(shmid, NULL, 0);

    // 3.读数据
    printf("%s\n", (char *)ptr);

    printf("按任意键继续\n");
    getchar();

    // 4.解除关联
    shmdt(ptr);

    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

Insert image description here
Insert image description here


key_t ftok(const char *pathname, int proj_id);

  • 作用:根据指定的路径名,和 int 值,生成一个共享内存的key
  • 参数
    • pathname:指定一个存在的路径
    • proj_idint 类型的值,但是这系统调用只会使用其中的1个字节
      范围:0-255一般指定一个字符’a’
  • 返回值:是一个 key_t 类型的键,表示关联到指定文件和项目ID的唯一键。

共享内存操作命令

ipcs 用法

打印当前系统中所有的进程间通信方式的信息

ipcs -a

打印出使用共享内存进行进程间通信的信息

ipcs -m

打印出使用消息队列进行进程间通信的信息

ipcs -q

打印出使用信号进行进程间通信的信息

ipcs -s

ipcrm 用法

移除用shmkey创建的共享内存段

ipcrm -M shmkey

移除用shmid标识的共享内存段

ipcrm -m shmid

移除用msqkey创建的消息队列

ipcrm -Q msgkey

移除用msqid标识的消息队列

ipcrm -q msqid

移除用semkey创建的信号

ipcrm -s semkey

移除用semid标识的信号

ipcrm -s semid

共享内存相关问题

问题1:操作系统如何知道一块共享内存被多少个进程关联?
:共享内存维护了一个结构体struct shmid_ds这个结构体中有一个成员 shm nattach 记录了关联的进程个数


问题2:可不可以对共享内存进行多次删除 shmctl
:可以的。因为 shmctl 标记删除共享内存,不是直接删除。那什么时候真正删除呢?
当和共享内存关联的进程数为0的时候,就真正被删除。当共享内存的key为0的时候,表示共享内存被标记删除了。如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。


共享内存和内存映射的区别

  1. 共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)

  2. 共享内存效果更高

  3. 内存
    所有的进程操作的是同一块共享内存。
    内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。

  4. 数据安全

    • 进程突然退出:共享内存还存在内存映射区消失
    • 运行进程的电脑死机,宕机了:数据存在在共享内存中,没有了。内存映射区的数据﹐由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
  5. 生命周期

    • 内存映射区:进程退出,内存映射区销毁
    • 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0)
      如果一个进程退出,会自动和共享内存进行取消关联。

守护进程

什么是控制终端

在 UNIX 系统中,用户通过终端登录系统后得到一个 shell 进程,这个终端成为 shell 进程的控制终端(controlling Terminal),进程中,控制终端是保存在 PCB 中的信息,而 fork() 会复制 PCB 中的信息,因此由 shell 进程启动的其它进程的控制终端也是这个终端。

默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。

在控制终端输入一些特殊的控制键可以给前台进程发信号,例如 Ctrl +C 会产生 SIGINT 信号, Ctrl +\ 会产生 SIGQUIT 信号。

Insert image description here


什么是进程组

进行组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程ID为该进程组的 ID,新进程会继承其父进程所属的进程组ID。

进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。

进程组和会话在进程之间形成了一种两级层次关系:

  • 进程组是一组相关进程的集合。
  • 会话是一组相关进程组的集合。

进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。


什么是会话

会话是一组相关进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会话 ID。新进程会继承其父进程的会话 ID。

一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端
在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程


进程组、会话、控制终端之间的关系


关系示例
以下命令的作用是在根目录下搜索所有文件和目录,将标准错误输出(STDERR)重定向到 /dev/null 忽略任何错误消息,然后通过管道将搜索结果的行数(即文件和目录的数量)计数,并在后台运行这个任务,期间允许继续使用终端。

find / 2> / dev /null | wc -l &

以下命令的作用是将 longlist 中的内容按照字母顺序排序,然后统计每个唯一项出现的次数,并以 次数 唯一项 的格式输出。

sort < longlist | uniq -c

以上两组命令关系图
Insert image description here


终端显示
Insert image description here

ps:执行完第一组命令后输入 fg ,命令将回到前台运行,并且可以看到它的输出,也可以在需要时终止它。


进程组、会话操作函数

// 获取调用进程的进程组ID
pid_t getpgrp (void) ;

// 获取指定进程的进程组ID
pid_t getpgid(pid_t pid) ;

// 设置指定进程的进程组ID
int setpgid(pid_t pid, pid_t pgid) ;

// 获取指定进程的会话ID
pid_t getsid(pid_t pid) ;

// 创建一个新的会话,并返回其会话ID
pid_t setsid (void) ;

什么是守护进程

守护进程(Daemon Process) ,也就是通常说的 Daemon进程(精灵进程),是Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,虽然产生条件和孤儿进程类似但并不是孤儿进程。

守护进程一般采用以 d 结尾的名字。
Linux的大多数服务器就是用守护进程实现的。比如, Internet服务器 inetd,web服务器 httpd等。

守护进程特征

  • 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。
  • 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如SIGINT、SIGQUIT)。

守护进程的创建步骤

  1. 执行一个 fork(),之后父进程退出,子进程继续执行。
  2. 子进程调用 setsid() 开启一个新会话。
  3. 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限。
  4. 修改进程的当前工作目录,通常会改为根目录 /
  5. 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。
  6. 在关闭了文件描述符0、1、2之后,守护进程通常会打开 /dev /null 并使用 dup2() 使所有这些描述符指向这个设备。
  7. 核心业务逻辑。

代码示例
写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。

#define _DEFAULT_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <string.h>
// 写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。

void work(int num)
{
    
    
    // 捕捉到信号之后,获取系统时间,写入磁盘文件
    time_t tm = time(NULL);
    struct tm *loc = localtime(&tm);
    // char buf[1024];
    // sprintf(buf, "%d-%d-%d %d: %d : %d\n", loc->tm_year, loc->tm_mon, loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);
    // print("%s\n", buf);

    char* str = asctime(loc);
    int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
    write(fd, str, strlen(str));

    close(fd);
}

int main(){
    
    
    // 1.创建子进程,退出父进程
    pid_t pid = fork();
    if(pid > 0){
    
    
        exit(0);
    }

    // 2.将子进程重新创建一个会话
    setsid();

    // 3.设置编码
    umask(022);

    // 4.更改工作目录
    chdir("/home/zxz/");

    // 5.关闭、重定向文件描述符
    int fd = open("/dev/null", O_RDWR);
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);

    // 6.业务逻辑

    // 6.1.捕捉定时信号
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    struct itimerval val;
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;

    // 6.2.创建定时器
    setitimer(ITIMER_REAL, &val, NULL);

    // 6.3.不让进程结束
    while (1)
    {
    
    
        sleep(10);
    }
    

    return 0;
}

Insert image description here
Insert image description here
vim进入time文件

vim time.txt

Insert image description here
键入 :e 重新加载文件Insert image description here


结语

The course notes are derived from the Niuke C++ job search project: Chapter 2 Linux Multi-process Development at https://www.nowcoder.com/courses/cover/live/504 . Since it is not a specialized process learning course, some of the teacher's explanations in this chapter may not be clear or complete. I studied it in combination with "CSAPP". Practicing the above questions will sometimes enlighten you. In addition, use it more to view the system API . document. This note contains additions and deletions relative to the course content. It is for personal review only and is for reference only if necessary. All the above codes and pictures are from the course and online resources. You are welcome to ask questions and suggestions. The more complete notes will be updated in real time. In order to further consolidate the relevant knowledge, the updates may be slower. We apologize for your understanding.
man

Guess you like

Origin blog.csdn.net/qq_53099212/article/details/132551062