Linux——进程控制

news2024/10/6 12:21:28

 

目录

1. 进程创建

1.1 fork函数

1.2 fork系统调用内部宏观流程

 1.3 fork后子进程执行位置分析

1.4 fork后共享代码分析

1.5 fork返回值

1.6 写时拷贝

1.7 fork常规用法

1.8 fork调用失败的原因

2.进程终止

2.1 进程退出场景

2.2 strerror函数—返回描述错误号的字符串

2.3 进程常见退出方法

2.4 _exit函数和exit函数

 2.5 return退出

3. 进程等待

3.1 进程等待必要性

3.2 进程等待方法

3.3 获取子进程status

3.4 进程阻塞等待和非阻塞等待

3.5 waitpid系统调用接口分析

3.6 阻塞等待代码和基于非阻塞调用的轮询检测方案

4. 进程程序替换

4.1 替换原理

4.2 替换函数

4.3 函数解释

4.4 命名理解

4.5 实现简易shell

5. 函数和进程之间的相似性:


1. 进程创建

1.1 fork函数

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1

1.2 fork系统调用内部宏观流程

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

fork创建子进程,系统中多了一个进程,及包含进程对应的PCB结构体以及对应的地址空间及页表映射关系,并将代码和数据加载到内存中,并将该进程加载到运行队列,等待操作系统调度器的调度!一旦该进程被调度起来,CPU就可以通过代码和数据及地址空间和页表映射,在物理内存找到对应的代码,进行运行!

 当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以 开始它们自己的旅程,看如下程序。

int main( void )
{
    pid_t pid;
    printf("Before: pid is %d\n", getpid());
    if ( (pid=fork()) == -1 )
        perror("fork()"),exit(1);
    printf("After:pid is %d, fork return %d\n", getpid(), pid);
    sleep(1);
    return 0;
} 


运行结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0

这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after 消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示

 所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

 1.3 fork后子进程执行位置分析

 fork前后,父子进程所有代码共享!

  • 代码汇编后,加载到内存,都有与之对应的地址
  • 因为进程随时可能被中断(可能并没执行结束),下次回来,还必须从之前的位置继续执行(不是最开始位置),就需要要求CPU必须随时记录当前进程执行的位置,所以,CPU有对应的寄存器数据EIP(PC指针,也叫做程序计数器,用于记录当前正在执行代码的下一行代码的地址),因此在进程切换时,需要将寄存器信息带走(上下文数据)。


寄存器在CPU内,只有一份(切换进程必须带走数据),寄存器内的数据,是可以有多份的(寄存器上下文数据

创建子进程的时候,也会将上下文数据给子进程(解释了fork之后,子进程不会从before处运行),虽然父子进程各自调度,各自会修改EIP,但是不重要了,子进程已经认为自己的EIP起始值,是fork之后的代码

1.4 fork后共享代码分析

创建子进程,给予进程分配对应的内核结构,必须子进程自己独有了,因为进程具有独立性!理论上,子进程也要有自己的代码和数据!可是一般而言,我们没有加载的过程,也就是说,子进程没有自己的代码和数据!!所以,子进程只能”使用“父进程的代码和数据!

代码:都是不可被写的,只能读取,所以父子共享,没有问题!

数据:可能被修改的,所以,必须分离!

对于数据而言:

  1. 创建进程的时候,就直接拷贝分离吗?(导致可能拷贝子进程根本就不会用到的数据空间,即便是用到,也可能只是只读)
  2. 编译器在编译时,尚且知道节省空间,操作系统同样如此,如分批加载挂起状态
  3. 因此创建子进程,不需要将不会被访问的或者只读取的数据,再去拷贝一份,浪费空间

但是,还有必须拷贝的数据,什么样的数据值得拷贝?将来会被父子进程写入的数据!!如上图g_val或者接收fork返回值的变量

  • 一般而言,即便是操作系统,也无法知道那些空间可能被写入!
  • 即便是提前拷贝了,也可能不会立马使用,因此造成空间浪费!
  • 所以操作系统提供了写时拷贝技术,来对父子进程的数据进行分离!

1.5 fork返回值

  • 子进程返回0,
  • 父进程返回的是子进程的pid

1.6 写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

 因为有写时拷贝技术的存在,所以,父子进程得以彻底分离!完成了进程的独立性的技术保证!(写时拷贝的好处)写时拷贝只在写入数据时发生拷贝,且对只读数据和代码不发生拷贝,节省空间!写时拷贝,是一种延时申请技术,可以提高整机内存的使用效率!

1.7 fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

1.8 fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

2.进程终止

进程终止时,操作系统本质上释放进程申请的相关内核数据结构和对应的代码和数据(本质上是释放资源)

2.1 进程退出场景

进程终止的常见方式?

        a. 代码跑完,结果正确

        b. 代码跑完,结果错误

        c. 代码没有跑完,进程崩溃(信号部分)

指针a和b方式,涉及问题:

main函数的返回值?main函数返回值的意义是什么?return 0; 含义是什么?为什么总是0?其他值是否可以?

main函数的返回值并不是总是0,返回0表示程序sucess,非0表示运行结果不正确。

意义一:main函数的返回值适用于返回上一级进程,用来评判该进程执行结果用的,为进程的退出码。

意义二:非零值有无数个,不同的非零值就可以标识不同的错误原因!在我们程序运行结束之后,结果不正确时,根据返回码,方便定位错误的原因细节!

2.2 strerror函数—返回描述错误号的字符串

#include <string.h>

       char *strerror(int errnum);

       int strerror_r(int errnum, char *buf, size_t buflen);
                   /* XSI-compliant */

       char *strerror_r(int errnum, char *buf, size_t buflen);
                   /* GNU-specific */
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<string.h>
int main(){
    printf("Hello world! pid = %d, ppid = %d\n", getpid(), getppid());
    for(int i = 0; i < 150; ++i){
        printf("%d: %s\n", i, strerror(i));
    }
    return 0;
}

2.3 进程常见退出方法

正常终止(可以通过 echo $? 查看进程退出码):

1. 从main返回

2. 调用exit

3. _exit

异常退出:

ctrl + c,信号终止

2.4 _exit函数和exit函数

_exit函数是系统提供的系统调用接口:

#include <unistd.h>
void _exit(int status);

参数:status 定义了进程的终止状态,父进程通过wait来获取该值

说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。

exit函数是C语言库提供的接口:

#include <unistd.h>
void exit(int status);

exit 或者 _exit 在任何地方调用,都表示直接终止进程!!!

Linux提供了系统调用接口_exit()函数在unistd.h头文件中,而C的库函数exit底层实则封装了系统调用接口_exit:

  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

测试代码:

int main()
{
 printf("hello");
 exit(0);
}
运行结果:
[root@localhost linux]# ./a.out
hello[root@localhost linux]#
int main()
{
 printf("hello");
 _exit(0);
}
运行结果:
[root@localhost linux]# ./a.out
[root@localhost linux]#

分析图:

 

 2.5 return退出

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返 回值当做 exit的参数。

有且只有在main函数内,return语句,就是终止进程的!return退出码!

3. 进程等待

3.1 进程等待必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3.2 进程等待方法

wait方法:

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
 成功返回被等待进程pid,失败返回-1。
参数:
 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

waitpid方法

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
 当正常返回的时候waitpid返回收集到的子进程的进程ID;
 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
 pid:
 Pid=-1,等待任一个子进程。与wait等效。
 Pid>0.等待其进程ID与pid相等的子进程。
 status:
 WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
 WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
 options:
 WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退 出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。
  • option默认为0,表示阻塞等待,status为输出型参数
  • waitpid(pid, NULL, 0) == wait(NULL)

3.3 获取子进程status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特 位):

 测试代码:

#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
     pid_t pid;
     if ( (pid=fork()) == -1 )
        perror("fork");
        exit(1);
     if ( pid == 0 ){
         //子进程休眠20秒,会变成僵尸进程
         //需要父进程等待回收
         sleep(20);
         exit(10);
     } else {
         //父进程
         int st;
         int ret = wait(&st);//阻塞等待
 
         if ( ret > 0 && ( st & 0X7F ) == 0 ){ // 正常退出
             //子进程退出码
             printf("child exit code:%d\n", (st>>8)&0XFF);
         } else if( ret > 0 ) { // 异常退出
             //子进程退出信号
             printf("sig code : %d\n", st&0X7F );
         }
     }
    return 0;
}

测试结果:

[root@localhost linux]# ./a.out #等20秒退出
 child exit code:10 
 [root@localhost linux]# ./a.out #在其他终端kill掉
 sig code : 9

进程异常退出,或者崩溃,本质是操作系统杀掉了你的进程!!!

操作系统如何杀掉进程呢!本质是通过发送信号的方式!

 父进程通过wait或者waitpid可以拿到进程的退出结果,为什么要用wait/waitpid函数呢!直接全局变量不行吗??

答案是不行,因为进程具有独立性,其创建子进程虽然具有相同虚拟内存地址,但是数据会发生写时拷贝,父进程无法获取到,况且如果是信号,更是无法获取!

既然进程具有独立性,进程退出码,不也是子进程的数据吗??父进程又凭什么拿到呢??wait/waitpid究竟干了什么呢??

因为wait、waitpid是系统调用接口,是系统创建的结构,可以访问内核,本质便是读取子进程的task_struct结构,此结构肯定包含了对应的信号码和退出码!

3.4 进程阻塞等待和非阻塞等待

options:WNOHANG选项,代表父进程非阻塞等待!本质为了避免魔术数字#define WNOHANG 1;

Linux C语言写的 -> 系统调用接口 -> OS自己提供的接口 ->就是C语言函数 -> 系统提供的一般大写的标记位 WNOHANG,其实就是宏定义!

WNOHANG —— Wait No Hang(等待过程中没夯住),夯住本质就是指这个进程没有被CPU调度,要么是在阻塞队列,要么是在等待被调度!

3.5 waitpid系统调用接口分析

阻塞等待和非阻塞等待,一般都是在内核中阻塞,等待被唤醒,如scanf和cin,底层必定封装了系统调用(阻塞等待),父进程通过调用waitpid来进行等待,如果子进程没有退出,waitpid这个系统调用,立马返回(非阻塞)

3.6 阻塞等待代码和基于非阻塞调用的轮询检测方案

阻塞等待:

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
int main(){
    pid_t id = fork();
    if(id < 0){
        perror("fork");
        exit(1);//表示进程运行完毕,结果不正确
    }else if(id == 0){
        //子进程
        int cnt = 5;
        while(cnt){
            printf("cnt: %d, 我是子进程, pid : %d, ppid : %d\n", cnt, getpid(), getppid());
            sleep(1);
            cnt--;
        }

        exit(111);
    }else{
        //父进程
        printf("我是父进程, pid = %d, ppid = %d \n",getpid(), getppid());
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        //printf("获取子进程退出信号: %d \n获取子进程退出码 %d\n", status & 0x7F,(status >> 8) & 0xFF);
        //pid_t ret = wait(NULL);
        //阻塞方式进行等待
        if(WIFEXITED(status)){
            printf("等待进程成功, ret = %d, 子进程退出码:%d\n", ret, WEXITSTATUS(status));
        }
    }
    return 0;
}

基于非阻塞调用的轮询检测方案:

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

typedef void(*handler_t)();
std::vector<handler_t> handlers;

void fun_one(){
    printf("这是一个临时任务1\n");
}
void fun_two(){
    printf("这是一个临时任务2\n");
}

void Load(){
    handlers.push_back(fun_one);
    handlers.push_back(fun_two);
}

int main(){
    pid_t id = fork();
    if(id == 0){
        //子进程
        int cnt = 5;
        while(cnt--){
            printf("我是子进程:%d\n", cnt);
            sleep(1);
        }
        exit(11);
    }else{
        //父进程
        int quit = 0;
        while(!quit){
            int status = 0;
            int res = waitpid(id, &status, WNOHANG);
            if(res > 0){
                printf("进程等待成功,退出状态码:%d\n", WEXITSTATUS(status));
                quit = 1;
            }else if(res == 0){
                printf("等待子进程退出,处理其他事情中......\n");
                if(handlers.empty()){
                    Load();
                }
                for(auto e : handlers){
                    e();
                }
            }else{
                printf("进程等待错误\n");
                quit = 1;
            }
            sleep(1);
        }
    }

    return 0;
}

4. 进程程序替换

4.1 替换原理

fork之后,父子进程各自执行代码的一部分—如果子进程就想执行一个全新的程序呢?

进程的程序替换,来完成这个功能!

程序替换,是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中!使得子进程拥有自己的代码!

 

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

进程替换的本质是加载新的代码和数据到内存,改变子进程页表的映射关系,完成替换

4.2 替换函数

其实有六种以exec开头的函数,统称exec函数:

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[]);
//使用可变参数列表

实际Linux提供了七个替换函数!额外一个

int execve(const char *path, char *const argv[], char *const envp[]);

4.3 函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值。

4.4 命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH

e(env) : 表示自己维护环境变量

 exec调用举例如下:

#include <unistd.h>
int main()
{
	char* const argv[] = { "ps", "-ef", NULL };
	char* const envp[] = { "PATH=/bin:/usr/bin", "TERM=console", NULL };
	execl("/bin/ps", "ps", "-ef", NULL);
	// 带p的,可以使用环境变量PATH,无需写全路径
	execlp("ps", "ps", "-ef", NULL);
	// 带e的,需要自己组装环境变量
	execle("ps", "ps", "-ef", NULL, envp);
	execv("/bin/ps", argv);

	// 带p的,可以使用环境变量PATH,无需写全路径
	execvp("ps", argv);
	// 带e的,需要自己组装环境变量
	execve("/bin/ps", argv, envp);
	exit(0);
}

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在 man手册第3节。这些函数之间的关系如下图所示。

下图exec函数族 一个完整的例子:

 

4.5 实现简易shell

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左 向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结 束。

 然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。 所以要写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)

 

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

#define NUM 1024
#define SIZE 32
char cmd_line[NUM];

void dealStr(char* str, char** argv) {
	char* dealstr = NULL;
	const char* sep = " ";
	size_t i = 0;
	for (dealstr = strtok(str, sep); dealstr != NULL; dealstr = strtok(NULL, sep)) {
		if (i < SIZE) {
			argv[i] = dealstr;
			i++;
		}
	}
    if(strcmp(argv[0], "ls") == 0){
        argv[i] = (char*)"--color=auto";
    }
	argv[++i] = NULL;
}

//shell运行原理:通过让子进程执行命令,父进程等待&&解析命令
int main(){
    while(1){
        //1.打印提示信息
        printf("[root@localhost myshell]#");
        fflush(stdout);
        //2.获取用户输入
        memset(cmd_line, '\0', sizeof cmd_line);
        char *g_argv[SIZE] = { NULL };
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL){
            continue;
        }
        cmd_line[strlen(cmd_line) - 1] = '\0';
        //printf("echo:%s\n", cmd_line);
        
        //3.命令行字符串分割
        dealStr(cmd_line, g_argv);     
        
        //4.TODO 内置命令,让父进程(shell)自己执行的命令,叫做内置命令,内建命令
        // 内建命令本质就是shell中的一个函数调用
        if(strcmp("cd", g_argv[0]) == 0){
            //not child execute, father execute
            if(g_argv[1] != NULL)
                chdir(g_argv[1]);
            continue;
        }

        //5.fork
        pid_t id = fork();
        if(id < 0){
            perror("fork");
            exit(1);
        }else if(id == 0){
            execvp(g_argv[0], g_argv);
            exit(1);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0){
            printf("exit code:%d -> result:%s\n",WEXITSTATUS(status), strerror(WEXITSTATUS(status)));
        }else{
            printf("wait fail!\n");
            exit(1);
        }
    }//end while
    return 0;
}

5. 函数和进程之间的相似性:

exec/exit就像call/return

一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。 这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程 之内的模式扩展到程序之间。

一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来 返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/812474.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Hadoop学习指南:探索大数据时代的重要组成——Hadoop概述

前言 在当今大数据时代&#xff0c;处理海量数据成为了一项关键任务。Hadoop作为一种开源的分布式计算框架&#xff0c;为大规模数据处理和存储提供了强大的解决方案。本文将介绍Hadoop的组成和其在大数据处理中的重要作用&#xff0c;让我们一同踏上学习Hadoop的旅程。 Hado…

信用卡欺诈检测案例 -- 机器学习项目基础篇(2)

这个案例面临的挑战是识别欺诈性信用卡交易&#xff0c;以便信用卡公司的客户不会因为他们没有购买的物品而被收取费用。 信用卡欺诈检测中涉及的主要挑战是&#xff1a; 每天都要处理大量数据&#xff0c;模型构建必须足够快&#xff0c;以便及时响应骗局。不平衡的数据&…

Edge解决默认Bing搜索跳转到国内版的问题

近期BingGPT申请通过以后&#xff0c;每次用PC端Edge去跳转的时候不管是否念Proxy咒都会进入国内版本Bing&#xff0c;排查得到默认Bing参数如下 因此&#xff0c;在edge://settings/searchEngines内添加搜索引擎&#xff0c;具体参数如下&#xff1a; 搜索引擎&#xff1a;必应…

嵌入式硬件系统的基本组成

嵌入式硬件系统的基本组成 嵌入式系统的硬件是以包含嵌入式微处理器的SOC为核心&#xff0c;主要由SOC、总线、存储器、输入/输出接口和设备组成。 嵌入式微处理器 每个嵌入式系统至少包含一个嵌入式微处理器 嵌入式微处理器体系结构可采用冯.诺依曼&#xff08;Von Neumann&…

算法之快速排序

快速排序广泛应用的主要原因是高效&#xff0c;核心算法是分而治之。Java标准库中Arrays类的sort方法里源码也正是使用了优化后的快速排序。 原理 快速排序的核心思想是分治&#xff1a;选择数组中某个数作为基数&#xff0c;通过一躺排序将要排序的数据分割成独立的两部分&a…

第十三届蓝桥杯复盘及未来规划

2022年4月28日 第十三届蓝桥杯结果揭晓 面对这份成绩&#xff0c;心里有些沉重 qwq 心里五味杂陈 当看到距离省1只差4个名额&#xff0c;当看到清晰又刺眼的’二等奖‘&#xff0c;当看到社群里面好多小伙伴拿到了省1的同时分享着喜悦&#xff0c;当想起自己从12月中旬开始写算…

密码学的一些常识

1&#xff0c;对称密码、公钥密码、消息认证、数字签名的对比 对称密码公钥密码发送者共享秘钥加密公钥加密接收者共享秘钥解密私钥解密秘钥配送问题存在不存在&#xff0c;但需要CA认证公钥机密性√√ 消息认证数字签名发送者共享秘钥计算MAC使用私钥对文本HASH值做签名接收者…

Linux 常用命令(快速复习)

基础命令 linux只有一个顶级目录/ ls命令 功能&#xff1a;列出文件夹信息 语法&#xff1a; ls [-l -h -a] [参数] 参数&#xff1a;被查看的文件夹&#xff0c;不提供参数&#xff0c;表示查看当前工作目录 -l&#xff0c;以列表形式查看 -h&#xff0c;配合-l&#xff0c;以…

如何手写一个@Async异步注解

如何手写一个Async异步注解 一、自定义注解二、编写AOP切面类三、编写测试类四、总结 一、自定义注解 自定义一个MyAsync注解&#xff0c;可以照抄Async Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) Documented public interface My…

【计算机视觉|人脸建模】深度学习时代的3D人脸重建调查报告

本系列博文为深度学习/计算机视觉论文笔记&#xff0c;转载请注明出处 标题&#xff1a;3D Face Reconstruction in Deep Learning Era: A Survey 链接&#xff1a;3D Face Reconstruction in Deep Learning Era: A Survey - PubMed (nih.gov) 摘要 随着深度学习的出现和图形…

ARM裸机-8

1、ARM的编程模式和工作模式 1.1、ARM的基本设定 ARM采用的是32位架构 ARM约定&#xff1a; - Byte&#xff1a;8 bits - Halfword &#xff1a;16 bits (2 byte) - Word&#xff1a;32 bits (4 byte) 大部分ARM core 提供&#xff1a; - ARM 指令集 (32-bit) - Thumb 指令集 …

TiProxy 原理和实现

说明 在上篇《TiProxy 尝鲜》 中做了一些实验&#xff0c;比如加减tidb节点后tiproxy可以做到自动负载均衡&#xff0c;如果遇到会话有未提交的事务则等待事务结束才迁移。 本次主要研究这样的功能在tiproxy中是如何实现的&#xff0c;本次分享内容主要为以下几部分&#xff…

数据结构基础知识、名词概述

1.1 基本概念和术语1.1.1 数据、 数据元素、 数据项和数据对象1.1.2 数据结构1.1.3 数据类型和抽象数据类型 1.2 抽象数据类型的表示与实现1.3 算法与算法分析&#xff08;1&#xff09;1.4 算法与算法分析&#xff08;2&#xff09;1.5 算法与算法分析&#xff08;3&#xff0…

gti 远程操作

目录 一. 分布式版本控制管理系统 1. 理解分布式版本控制管理系统 二. 创建远程仓库 ​编辑 ​编辑 三. 克隆远程仓库_HTTP 四. 克隆远程仓库_SSH 配置公钥 添加公钥 五. git 向远程仓库推送 六. 拉取远程仓库 七. 忽略特殊文件 八. 配置别名 一. 分布式版本控制管理…

JVM内存分配机制详解

文章目录 一、对象的创建流程1、类加载检查2、分配内存3、初始化4、设置对象头5、执行方法 二、对象内存分配1、栈上分配2、对象在Eden区分配3、大对象直接进入老年代4、长期存活的对象进入老年代5、对象动态年龄判断6、老年代空间分配担保机制 三、对象内存回收1、引用计数法2…

别再分库分表了,试试TiDB!

什么是NewSQL 传统SQL的问题 升级服务器硬件 数据分片 NoSQL 的问题 优点 缺点 NewSQL 特性 NewSQL 的主要特性 三种SQL的对比 TiDB怎么来的 TiDB社区版和企业版 TIDB核心特性 水平弹性扩展 分布式事务支持 金融级高可用 实时 HTAP 云原生的分布式数据库 高度兼…

RT1052 的四定时器

文章目录 1 Quad Timer&#xff0c;简称&#xff1a;QTMR2 单个通道的框图3 QTMR配置3.1 QTMR1 时钟使能。3.2 初始化 QTMR1。3.2.1 QTMR_Init 3.3 设置 QTMR1 通道 0 的定时周期。3.3.1QTMR_SetTimerPeriod 3.4 使能 QTMR1 通道 0 的比较中断。3.4.1 QTMR_EnableInterrupts 3.…

14. Spring AOP 的组成和实现

目录 1. Spring AOP 简介 2. AOP 的组成 2.1 切面&#xff08;Aspect&#xff09; 2.2 连接点&#xff08;Join Point&#xff09; 2.3 切点&#xff08;Pointcut&#xff09; 2.4 通知&#xff08;Advice&#xff09; 3. Spring AOP的实现 3.1 新建项目 3.2 添加 AOP …

Zebec Card 将在亚洲、拉美等地区推出,生态全球化加速

随着以Visa、特斯拉、BNY Mellon、BlackRock、Mastercard、Gucci等为代表的传统商业机构巨头&#xff0c;以及萨尔瓦多、中非共和国等为代表的国家不断的向加密货币领域布局&#xff0c;越来越多的投资者开始以新的眼光来看待加密货币&#xff0c;仅在2022年&#xff0c;加密货…

1400*B. I Hate 1111(思维+数学)

Example input 3 33 144 69 output YES YES NO 题意&#xff1a; 问一个数字是否可以由 11&#xff0c;111&#xff0c;1111&#xff0c;11111...... 任意倍数加和所得。 解析&#xff1a; 可以观察到 1111%110&#xff0c;11111%1110&#xff0c;而后面更大的11111111…