【Linux】进程创建|进程终止|进程等待|进程程序替换

news2024/11/18 9:23:34

索引

  • 1.进程创建
    • fork函数初识
      • 😊我们先来看这样的一个程序:
    • 写时拷贝
    • fork返回值的三个问题
  • 2.进程终止
    • 进程退出场景
    • 进程常见退出方法
      • 进程退出码:
  • 3.进程等待
    • 进程等待的方法
      • wait方法
      • waitpid方法
      • 获取子进程status
    • 进程的阻塞等待方式:
    • 进程的非阻塞等待方式:
  • 4.进程程序替换
    • 是什么?
    • 为什么?
    • 怎么做?
      • execl(l:列表式的将参数一个一个的传入)
      • execlp(p:path)
      • execv(v:vector)
      • execvp(v:vector p:path)
      • execle(e:环境变量)
      • execvpe(v:vector p:path e:环境变量)
      • execve
  • 5.制作一个简易的shell

1.进程创建

fork函数初识

在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
在这里插入图片描述
返回值:给子进程返回0,给父进程返回子进程id,出错返回-1

当一个进程调用fork之后,就有两个二进制代码相同的进程,相当于从一个执行流变成两个执行流 了。但每个进程都将可以开始它们自己的旅程,看如下程序。

😊我们先来看这样的一个程序:

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

int global_value = 100;

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        printf("fork error\n");
        return 1;
    }
    else if(id == 0)
    {
        int cnt = 0;
        while(1)
        {
            printf("我是子进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value);
            sleep(1);
            cnt++;
            if(cnt == 10)
            {
                global_value = 300;
                printf("子进程已经更改了全局的变量啦..........\n");
            }
        }
    }
    else
    {
        while(1)
        {
            printf("我是父进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value);
            sleep(2);
        }
    }
    sleep(1);
}

执行结果:

在这里插入图片描述

fork之前父进程单独执行,fork之后,父子两个执行流分别执行。注意:fork之后,谁先执行完全由调度器决定。
对于上面的执行结果:
在子进程没有改变全局变量global_value的时候,可以看到父子进程之前的global_value以及global_value的地址都是一样的;
在子进程改变全局变量global_value之后,可以看到父子进程的值有了区别,子进程的值是300,父进程是100,因为我们知道进程具有独立性,再往后看奇怪的事情发生了,为什么父子进程的这个全局变量global_value的地址是一样的呢?
那么下面就介绍一下写时拷贝。

写时拷贝

一般情况下父子代码共享,当父子再不写入时,数据也是共享的。当任意一方试图对共享的数据进行修改,那么便以写时拷贝的方式给这个进程复制一份副本。具体见下图蓝色区域:
在这里插入图片描述
所以子进程修改global_value的内容之后,父子进程表面上global_value的虚拟地址是一样的,但是实际映射到物理内存上是不一样的。

fork返回值的三个问题

1️⃣1.如何理解fork函数有两个返回值问题?
因为没有调用fork的时候,只有父进程,调用fork之后,子进程也就被创建出来了(创建的过程特别复杂),父子进程两个执行流,两套代码,两个return,那么就意味着有两个返回值。

3️⃣2.如何理解fork返回之后,给父进程返回子进程pid,给子进程返回0?
生活中正常来讲父亲:孩子是1:n的关系(n>=1),对于父进程来说,父亲找孩子不具有唯一性。而对于子进程来说,孩子找父亲是具唯一性的。所以才会给父进程返回子进程pid,给子进程返回0。

3️⃣3.对于上面的例子代码,如何理解同一个id值,怎么可能会保存两个不同的值,让if和else if同时执行?
两个返回值那么就意味着同一个id要被父子进程的返回值赋值(写入)两次,所以先对id进行赋值(写入)的进程就正常赋值(写入),后对id进行赋值(写入)的进程因为进程具有独立性,那么就会发生写时拷贝,典型的同一个id,虚拟地址一样,但是内容却不一样。

  • fork常规用法
    1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,需要生成子进程来处理请求。
    2.一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
  • fork调用失败的原因
    1️⃣系统中有太多的进程2️⃣实际用户的进程数超过了限制。

2.进程终止

进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

进程常见退出方法

进程退出码:

写代码是为了完成某件事情,如何得知我的代码运行的如何呢?——进程退出码
进程退出的时候,都有对应的退出码,作用是标定进程执行的结果是否正确。一般而言,退出码,都必须有对应的退出码的文字描述,1. 可以自定义 2. 可以使用系统的映射关系(不太频繁)

如何设定main函数返回值呢?如果不关心进程退出码,return 0就行。如果未来我们是要关心进程退出码的时候,要返回特定的数据表明特定的错误。
0:success, !0:标识失败, !0具体是几,标识不同的错误
在C语言中有strerror和perror函数可以自动获取错误码,然后打印对应的错误信息。


  • 正常终止:
    1.从main返回退出码(return 0这个0就是退出码)
    2.调用exit(exit(23)这个23就是退出码)
    3._exit(_exit(45)这个45就是退出码)

当一个进程正常终止的时候,紧接着输入 echo $? 就可以把最近一次return或者exit和_exit的退出码打印出来。

  • 异常退出:
    ctrl + c,信号终止

  • _exit函数(是系统调用)
    头文件:#include <unistd.h>
    函数原型:void _exit(int status);
    参数:status 定义了进程的终止状态,父进程通过wait来获取该值(下面进程等待会有讲解)。
    说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行echo $?发现返回值是255。
  • exit函数(是C语言的库函数)
    头文件:#include <unistd.h>
    函数原型:void exit(int status);
    exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
    1.执行用户通过 atexit或on_exit定义的清理函数。
    2.关闭所有打开的流,所有的缓存数据均被写入
    3.调用_exit

用代码对比exit()和_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]#

很明显的区别就是退出的时候刷不刷新缓冲区的问题。

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

3.进程等待

进程等待必要性:
父子进程中,如果子进程退出,子进程会把对应的退出信息存到子进程的PCB里,需要父进程来处理,但如果父进程不管不顾,就可能造成子进程变为僵尸进程,进而造成内存泄漏。进程一旦变成僵尸状态,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
另外,父进程派给子进程的任务完成的如何,例如,子进程是否运行完成?运行结果对还是不对?是否正常退出?这些我们都需要知道。
那么这里就要用到进程等待来解决僵尸进程以及获取子进程信息的问题了。
父进程通过进程等待的方式:回收子进程资源,获取子进程退出信息

进程等待的方法

wait方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);

返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,通过传入的status获取子进程退出状态,不关心则可以设置成为NULL。
如果终止了多个子进程,则wait()将获取任意的子进程并返回该子进程的进程ID。

waitpid方法

pid_ t waitpid(pid_t pid, int *status, int options);

检测值进程退出信息,将子进程退出信息通过status拿出来。

  • 返回值:
    当正常返回的时候waitpid返回收集到的子进程的进程ID;
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
  • 参数:
    pid:
    Pid=-1,等待任一个子进程。与wait等效。
    Pid>0.等待其进程ID与pid相等的子进程。
    status:
    可以直接用status位段的特性来判断进程的退出信息。(下面获取进程status有介绍)
    也可以用Linux提供的这两个宏通过status来判断状态
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
    options:
    0:让waitpid使用阻塞等待的方式运行
    WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

获取子进程status

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

通过status就可以体现上面的几种运行情况:
在这里插入图片描述
退出状态(8到15位):退出码(结果是否正确)
终止信号(0到7位):退出信息,一个进程如果出异常了。那么一定是该进程收到了对应的信号。包括除0或者野指针,其实是属于操作系统识别到了这个进程有问题,然后给这个进程发信号。换句话说我们是可以通过终止信号来得知这个进程是否正常退出。

通过kill -l 就可以看对应的退出信号,退出信号如果是0那么说明没有问题。如果退出信号非零。那么就根据下面的序号去寻找对应的错误即可。
在这里插入图片描述

进程的阻塞等待方式:

  • 如果子进程先父进程一步退出,父进程还没有运行到wait,那么子进程在这段时间就会变为僵尸状态,直到父进程运行到wait,然后父进程就会回收子进程的资源然后通过传入的status值获取子进程的退出信息。
  • 如果父进程已经运行到wait,子进程还在正常运行中,那么父进程这时就会停下来等待,直到子进程运行完毕,父进程通过wait回收了子进程的资源、获取了子进程的退出信息之后。父进程才会接着运行wait下面的代码。
  • 如果不存在该子进程,则立即出错返回。

在这里插入图片描述

有了上面的基础接下来看一下waitpid函数的应用。

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        //子进程
        int cnt = 5;
        while (cnt)
        {
            printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
            int* p = NULL;//野指针错误
            *p = 100;
        }
        exit(12); //进程退出
    }
    // 父进程
    int status = 0; // 不是被整体使用的,有自己的位图结构
    pid_t ret = waitpid(id, &status, 0);
    if (id > 0)
    {
        printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status >> 8) & 0xFF);
    }

    sleep(5);
}    
  • 当把上面的野指针错误屏蔽的话,正常终止信号就是0,退出码就是我们自己设定的exit()里的数字12。
    在这里插入图片描述
  • 当进程有野指针错误,那么终止信号就会显示11,从kill -l我们可以知道11号是段错误(段错误就是指访问的内存超出了系统所给这个程序的内存空间),指的就是这里的野指针错误。
    只要有了错误那么退出码就无意义,退出码系统就不再写入了,退出码会显示0。
    在这里插入图片描述
  • 如果提前用kill命令杀死子进程,那么用kill -n 进程id杀掉子进程,那么退出信号就是n。退出码无意义还是0.
    在这里插入图片描述

进程的非阻塞等待方式:

把waitpid的option选项换成WNOHANG此时的waitpid就是非阻塞等待的方式了,检测id的进程状态如果没有就绪,直接返回,接着执行下面的代码,不会一直卡住去等待,可以用while循环来控制多次检测,每一次都是非阻塞等待。那么多次非阻塞等待就称为轮询。
相比于阻塞等待,非阻塞等待不会占用父进程的所有精力。可以在轮询期间去干一些别的事情。
示例代码:

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

#define NUM 10

typedef void (*func_t)(); //使用typedef把func_t定义成一个函数指针类型

func_t handlerTask[NUM];

//样例任务
void task1()
{
    printf("handler task1\n");
}
void task2()
{
    printf("handler task1\n");
}
void task3()
{
    printf("handler task1\n");
}

void loadTask()
{
    memset(handlerTask, 0, sizeof(handlerTask));
    handlerTask[0] = task1;
    handlerTask[1] = task1;
    handlerTask[2] = task1;
}


int main()
{
    pid_t id = fork();
    assert(id != -1);
    if (id == 0)
    {
        //child
        int cnt = 3;
        while (cnt)
        {
            printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
           
        }

        exit(10);
    }

    loadTask();
    // parent
    int status = 0;
    while (1)
    {
        pid_t ret = waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞-> 子进程没有退出, 父进程检测时候,立即返回
        if (ret == 0)
        {
            // waitpid调用成功 && 子进程没退出
            //子进程没有退出,我的waitpid没有等待失败,仅仅是监测到了子进程没退出.
            printf("wait done, but child is running...., parent running other things\n");
            for (int i = 0; handlerTask[i] != NULL; i++)
            {
                handlerTask[i](); //采用回调的方式,执行我们想让父进程在空闲的时候做的事情
            }
        }
        else if (ret > 0)
        {
            // 1.waitpid调用成功 && 子进程退出了
            printf("wait success, exit code: %d, sig: %d\n", (status >> 8) & 0xFF, status & 0x7F);
            break;
        }
        else
        {
            // waitpid调用失败
            printf("waitpid call failed\n");
            //    break;
        }
        sleep(1);
    }
    return 0;
}

运行结果:
在这里插入图片描述

4.进程程序替换

是什么?

程序替换的本质就是将指定程序的代码和数据加载到指定的位置。覆盖自己原有的代码和数据。

为什么?

之前我们创建子进程只能执行父进程代码的一部分,现在我们创建出来子进程想让子进程执行一个全新的程序的话就需要用到进程程序替换。

怎么做?

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

RETURN VALUE
The exec() functions return only if an error has occurred.
The return value is - 1, and errno is set to indicate the error.
  • 如果调用出错则返回-1
  • 只要是一个函数,调用就有可能失败,就是没有替换成功(没有替换)。
  • 为什么没有成功返回值呢?因为成功了,就和接下来的代码无关了,判断毫无意义。只要返回了,那就一定是错误了。

execl(l:列表式的将参数一个一个的传入)

格式:

int execl(const char *path, const char *arg, ...);

用法:

execl("/usr/bin/ls"/*要执行哪一个程序*/, 
       "ls", "--color=auto", "-a", "-l", NULL/*你想怎么执行*/);
        // 所有的exec函数都以null结尾

perror("ececl"); //打印错误原因

下面是调用成功的例子,可以看到我们设置的退出码123在程序被正常替换之后就没有被执行了。

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

int main(int argc, char* argv[])
{
    printf("process is running...\n");
    pid_t id = fork();
    assert(id != -1);

    if (id == 0)
    {
        // 这里的替换,会影响父进程吗?不会的因为进程具有独立性
        // 类比:命令行怎么写,这里就怎么传
        sleep(1);
        // ./exec ls -a -l -> "./exec" "ls" "-a" "-l"
        
        execl("/usr/bin/ls", "ls", "-a", "-l", "--color=auto", NULL);
        
        exit(123); //能运行到这里就必然是替换失败了
    }

    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret > 0) printf("wait success: exit code: %d, sig: %d\n", 
    (status >> 8) & 0xFF, status & 0x7F);

}

在这里插入图片描述
对于上述代码如果把ls路径故意写错,必然函数调用失败。如果我们自己通过exit或者return设置退出码了,那么父进程wait之后获取的就是设置好的return或者exit的值,如果没有设置并且函数调用失败,那么就是-1,我们知道在计算机中数值是以补码的形式存储的,那么-1在计算机中就是全1,全1的退出码通过status的8位退出码信息打印出来就是255.
在这里插入图片描述

而且父子进程中,当子进程进行程序替换的时候,对父进程会造成影响。那么这时候就要发生程序的写时拷贝了,以此来保证进程间的独立性。

execlp(p:path)

p:path:带p字符的函数,不用告诉程序的路径,你只要告诉要执行谁,它就会自动在环境变量PATH,进行可执行程序的查找!

格式:

int execlp(const char *file, const char *arg, ...);

使用举例:

execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);

这里有两个ls,一个是告诉系统我要执行谁,一个是告诉系统怎么执行。

execv(v:vector)

v:vector可以将所有的执行参数,放入数组中,统一传递,而不用进行使用可变参数列表方案。

格式:

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

使用举例:

char *const argv_[] = {"ls","-a", "-l","--color=auto",NULL};
execv("/usr/bin/ls", argv_);

execvp(v:vector p:path)

对于v和p上面两个函数有所介绍,这里就不多赘述了。
格式:

int execvp(const char *file, char *const argv[]);

使用举例:

char *const argv_[] = {"ls","-a", "-l","--color=auto",NULL};
execvp("ls", argv_);

execle(e:环境变量)

格式:

int execle(const char *path, const char *arg, ...,char *const envp[]);

mybin.c:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    // 系统就有
    printf("PATH:%s\n", getenv("PATH"));
    printf("PWD:%s\n", getenv("PWD"));
    // 自定义
    printf("MYENV:%s\n", getenv("MYENV"));

    return 0;
}

myexec.c:

char *const envp_[] = {
            (char*)"MYENV=11112222233334444",
            NULL
};

extern char **environ;
//execle("./mybin", "mybin", NULL, envp_); //自定义环境变量指针

putenv((char*)"MYENV=4443332211"); 
//将指定环境变量导入到系统中 environ指向的环境变量表

execle("./mybin", "mybin", NULL, environ); //默认的环境变量指针
//实际上,默认环境变量你不传,子进程也能获取

最终看到的效果就是在myexec.c中通过execle函数调用mybin然后将系统的和自定义的环境变量都打印出来了

我们平时想要运行一个程序,就要先加载程序到内存中,那么加载程序它其实用到的是exec函数(加载器),所以exec函数先于main函数执行,而且默认的环境变量通过exec函数就已经传入到main程序里了,所以我们在执行main程序的时候可以直接使用默认的环境变量。
但是自定义的环境变量就需要使用putenv来将其导入到系统中。然后才能通过environ指针来获取。

execvpe(v:vector p:path e:环境变量)

和上面的几个exec函数功能有相似之处,这里不过多介绍。

int execvpe(const char* file, char* const argv[], char* const envp[]);

execve

程序替换中execve才是真正的系统调用,其他的六个都是封装,为了让我们有很多的选择性。
在这里插入图片描述


⚠️值得注意的是:可以使用程序替换,调用任何后端语言对应的可执行程序
下面是在C语言下分别调用C++、python、shell等语言的代码片段。

execl("./mybin", "mybin", NULL);
execl("./mypy.py", "mypy.py", NULL);
execl("./myshell.sh", "myshell.sh", NULL);

5.制作一个简易的shell

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

#define NUM 1024
#define OPT_NUM 64

char lineCommand[NUM];
char *myargv[OPT_NUM]; //指针数组
int  lastCode = 0;
int  lastSig = 0;

int main()
{
    while(1)
    {
        // 输出提示符
        printf("用户名@主机名 当前路径# ");
        fflush(stdout);//刷新缓冲区

        // 获取用户输入, 输入的时候,输入\n
        char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
        assert(s != NULL);
        (void)s;
        
        // 一般我们输入完毕回车之后自动带一个\n,要清除最后一个\n,否则不好看
        lineCommand[strlen(lineCommand)-1] = 0; // ?
        //printf("test : %s\n", lineCommand);
        
        // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
        // 字符串切割
        myargv[0] = strtok(lineCommand, " ");
        
            //对于ls命令需要做的一点小修改
	        int i = 1;
	        if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
	        {
	            myargv[i++] = (char*)"--color=auto";
	        }

        // 进行连续分隔:如果没有子串了,strtok->NULL, myargv[end] = NULL
        while(myargv[i++] = strtok(NULL, " "));
		
		//对于cd命令需要做的一点小修改(chdir的使用)
        // 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
        // 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
        if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            if(myargv[1] != NULL) chdir(myargv[1]);
            continue;
        }
        
        //对于echo命令需要做的一点小修改,这里就能解释为什么
        if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
        {
            if(strcmp(myargv[1], "$?") == 0)
            {
                printf("%d, %d\n", lastCode, lastSig);
            }
            else
            {
                printf("%s\n", myargv[1]);
            }
            continue;
        }

        // 这里是一个测试:测试是否成功, 条件编译
#ifdef DEBUG
        for(int i = 0 ; myargv[i]; i++)
        {
            printf("myargv[%d]: %s\n", i, myargv[i]);
        }
#endif
        // 内建命令 --> echo

        // 执行命令
        pid_t id = fork();
        assert(id != -1);

        if(id == 0)
        {
            execvp(myargv[0], myargv);
            exit(1);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        assert(ret > 0);
        (void)ret;
        lastCode = ((status>>8) & 0xFF);
        lastSig = (status & 0x7F);
    }
}

补充:

  • 什么是当前路径?
    当前路径就是cwd所标识的路径。也就是当前进程的工作目录。
    exe所指的是当前进程执行的是磁盘路径下的哪一个程序。
    在这里插入图片描述

当fork()之后,子进程执行的cd->子进程有自己的工作日录->更改的是子进程的目录!->子进程执行完毕->继续用的是父进程,即shell。

  • 我们自己写shell的时候,怎么使用cd来使它的当前路径变化呢?
    可以看到需要使用chdir就可以解决这个问题。在上面代码的注释中有相应的解释。
    在这里插入图片描述

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

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

相关文章

vue实现导入表格数据【纯前端实现】

一、文章引导 #mermaid-svg-3VJi5rNvrLDOy2MT {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3VJi5rNvrLDOy2MT .error-icon{fill:#552222;}#mermaid-svg-3VJi5rNvrLDOy2MT .error-text{fill:#552222;stroke:#55222…

WSL Ubuntu SSH

WSL中的IP wsl中的ubuntu的ip是动态分配的&#xff0c;每次开机都不一样&#xff0c;而且动态分配的ip和windows系统中的ip不在同一网段&#xff0c;但是我发现在windows中能ping通wsl中ubuntu的ip&#xff0c;这说明子系统与虚拟机不同&#xff0c;在查看ubuntu系统ip时&…

第010课 - docker安装mysql

第010课 - docker安装mysql docker run -p 3306:3306 --name mysql \ -v /mydata/mysql/log:/var/log/mysql \ # 这个里面是容器内mysql相关的日志 -v /mydata/mysql/data:/var/lib/mysql \ # 这个里面是msyql数据相关的内容 -v /mydata/mysql/conf:/etc/mysql \ # 这个里面是容…

数据结构进阶 二叉树OJ题

作者&#xff1a;小萌新 专栏&#xff1a;数据结构进阶 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;介绍几道二叉树的oj题 二叉树OJ题题目一 根据二叉树创建字符串题目二 二叉树的层序遍历题目三 二叉树的最近公共祖先题目一 根据…

华为机试 HJ35 蛇形矩阵

华为机试 HJ35 蛇形矩阵[HJ35 蛇形矩阵](https://www.nowcoder.com/practice/649b210ef44446e3b1cd1be6fa4cab5e)方法一&#xff1a;顺序填表方法2&#xff1a;数学规律HJ35 蛇形矩阵 描述 蛇形矩阵是由1开始的自然数依次排列成的一个矩阵上三角形。 例如&#xff0c;当输入5…

【数据结构】链表基础知识讲解

文章目录链表链表的结构使用链表的优点模拟实现链表链表 在之前的学习中我们讲解了顺序表ArrayList&#xff0c;Java模拟实现顺序表&#xff0c;如果需要大家可以去看一看&#xff0c;顺序表底层的实现逻辑其实就是数组&#xff0c;在物理存储结构和逻辑上都是连续的&#xff…

Eth 03 -以太网驱动Eth的配置

以太网的配置,下面这张图描述了以太网的配置参数: EthCtrlConfig:单个控制器的配置EthCtrlEnableMii :启用/禁用用于收发器访问的媒体独立接口 (MII)EthCtrlEnableRxInterrupt:启用/禁用接收中断EthCtrlEnableTxInterrupt:启用/禁用传输中断EthCtrlIdx:指定已配置控制…

【BUUCTF】MISC(第一页wp)

文章目录签到金三胖二维码你竟然赶我走大白N种方法解决乌镇峰会种图基础破解wireshark文件中的秘密图片exifLSBLSB隐写&#xff08;最低有效位隐写&#xff09;&#xff1a;zip伪加密ZIP 文件由**三个部分**组成&#xff1a;**压缩源文件数据区**&#xff1a;**压缩源文件目录区…

FS4412环境搭建

目录 一、开发板硬件资源介绍 二、交叉开发环境 2.1安装交叉编译工具链 2.2配置全局变量​编辑 2.3测试​编辑 2.4终端 2.5安装串口驱动 2.6上电测试 三、地址映射表 一、开发板硬件资源介绍 中间红色的是samsung的主控&#xff0c;四个粉色的256M的内存条&#xff0…

STM32F4SysTick记录

滴哒主要用于延时和实时系统 模板为原子串口实验源码&#xff0c;入口为24行 120行为滴哒定时器的CTRL寄存器位时钟源设置 这个参数的必要性是用于溢出时间的计算参数之一 可以设置为HCLK或HCLK的8分频 延时函数理解 设置LOAD是设置重装载值 设置VAL清空计数值以及标志位 …

强大的ANTLR4(2)

每次在命令行里输入文本有点麻烦&#xff0c;可以将hello slb保存于hello.txt文本文件中&#xff0c;然后运行命令&#xff1a; antlr4-parse Hello.g4 r -tokens hello.txt出现如下内容&#xff1a; [0,0:4hello,<hello>,1:0] [1,6:8slb,<ID>,1:6] [2,9:8<EO…

JDBC开荒

docker 创建MySQL 一、简介 Java DataBase Connectivity &#xff0c;是Java程序访问数据库的标准接口 Java访问DB的时候&#xff0c;并不是直接通过TCP连接的&#xff0c;而是通过JDBC接口&#xff0c;而JDBC接口又是通过JDBC驱动来访问的 JDBC是Java标准库自带的&#xff0…

(HP)next.js入门

推荐文档&#xff1a;生成<head> - 《next.js v7.0 中文文档》 - 书栈网 BookStack 1&#xff0c;解决的问题 SPA单页面应用的两个问题&#xff1a;首屏加载过慢&#xff0c;不能SEO(搜索引擎抓取&#xff09; 2&#xff0c;它是一个react服务端渲染框架 3&#xff0c;…

ArcGIS去除黑边方法汇总

概述 在使用ArcGIS对影像进行应用的时候&#xff0c;如果出现了黑边&#xff0c;除了影响美观之外&#xff0c;进行镶嵌处理也可能会有问题&#xff0c;这里&#xff0c;我们介绍一下几种ArcGIS去除黑边的方法&#xff0c;希望能够对大家有所帮助。 数据来源 教程所使用的实…

【C++进阶】类型转换

&#x1f387;C学习历程&#xff1a;入门 博客主页&#xff1a;一起去看日落吗持续分享博主的C学习历程博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 也许你现在做的事情&#xff0c;暂时看不到成果&#xff0c;但不要忘记&…

蓝桥杯Python练习题8-查找整数

资源限制   内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述   给出一个包含n个整数的数列&#xff0c;问整数a在数列中的第一次出现是第几个。 输入格式   第一行包含一个整数n。  …

欢迎谷歌回归中国,但有前提!李彦宏也发了条朋友圈

2018年&#xff0c;Google部分功能成功回归中国大陆 周一&#xff0c;人民日报在海外社交媒体平台Twitter和Facebook上刊文&#xff0c;针对谷歌计划以过滤版搜索引擎重返中国大陆的消息回应称&#xff0c;欢迎谷歌重返中国大陆&#xff0c;但前提是必须遵守中国法律。李彦宏在…

云计算IaaS、PaaS(iPaaS/aPaaS)以及SaaS以及发展趋势

一、云计算IaaS、PaaS以及SaaS架构 云计算涉及了很多产品与技术&#xff0c;表面上看起来的确有点纷繁复杂&#xff0c;但是云计算本身还是有迹可循和有理可依的&#xff0c;下面介绍一套云计算的架构&#xff0c;具体请看图&#xff1a; 上面这个云架构共分为服务和管理这两…

车载入侵检测与防御系统介绍

作者 | 田铮 上海控安可信软件创新研究院项目经理 来源 | 鉴源实验室 引言&#xff1a;上一篇文章&#xff08;智能网联汽车网络安全攻击与防御技术概述&#xff09;介绍了智能网联汽车中的网络安全攻击案例和具体攻击类型。而本篇文章中&#xff0c;我们将对汽车网络安全风险…

SpringSecurity——OAuth2框架鉴权实现源码分析

SpringSecurity——OAuth2框架鉴权实现源码分析一、ManagedFilter迭代过滤器链1.4 springSecurityFilterChain1.4.7 OAuth2AuthenticationProcessingFilter①.OAuth2AuthenticationProcessingFilter.class②.CookieTokenExtractor.class&#xff08;我们自己重写的方法&#xf…