内核模块里获取当前进程和父进程的cmdline的方法及注意事项,涉及父子进程管理,和rcu的初步介绍

news2024/11/30 17:16:39

一、背景

在编写内核态系统监控代码时,有时候为了调试的便捷性,不仅要拿到异常事件有关的线程id,进程id和父进程id,还需要拿到当前进程和父进程的comm和cmdline。主要有几下几个原因:

1)单纯的pid或者tgid其信息本身并不能给我们多少线程有关的有效信息,除了一些系统的内核线程的pid是固定的以外,其他的线程的pid都是每次运行会发生变化的,这些信息如果不配合其他的监控信息就不能独立作为详细的调试依据

2)如果仅仅是获取comm也就是不带上args参数(cmdline当然是带上args参数的完整的命令)的话,那么其信息一般也是非常有限的,因为启动程序如果不是fork运行的话,都是要通过shell来解释运行,这时候该进程一开始运行的时候comm就继承了父进程的comm也就是bash,什么具体信息也提供不了,当然后面通过泛exec的系统调用变成了实际运行的程序以后,光一个程序名字有时候信息还是不够的,比如如果是运行一个脚本,如执行sudo python3 xxx,其实它是父子两个进程,父进程的comm是sudo,子进程的comm是python3,那么这时候如果只是抓comm信息的话,就算也同时抓父进程的comm,那也只能拿到sudo和comm这两个名字,也不知道到底运行的什么python3脚本。如下截图:

但如果去捞cmdline信息的话,你就能拿到详细的信息,能大致知道是跑的什么程序:

接下来,我们将在第二章里给出内核模块里获取到当前进程和父进程的cmdline方法,以及非常重要的注意事项,另外还会涉及父子进程管理的一些细节,并对用这些细节做了不少验证的实验,包括孤儿进程的归属问题,如child_subreaper的细节。在第三章里会贴一张第二章里讲到的获取进程和父进程的cmdline方法时用到的rcu这个内核重要的高性能神器。第三章里只会简单介绍一下rcu,关于rcu的复杂和繁琐的细节,后面的博客会逐一展开,我会在第三章里贴出总结的rcu的一些概念的思维导图截图,先对rcu相关的内容起一个头。注意,本文还并不涉及rt-linux下的rcu,rt-linux下的rcu比普通linux下的rcu在管理上更加复杂,暂且先不涉及。

本文有不少篇幅在进程线程管理细节上,尤其父子进程管理,相关的进程/线程管理的之前博文,可以参考 进程/线程创建和退出事件的捕获_for (inti=0;i<2;++i){ threads.emplace back(worker,-CSDN博客。

二、内核模块里获取当前进程和父进程的cmdline的方法

我们先在2.1一节里看一下核心代码,对代码进行一些说明,然后在2.2一节里讲一下注释事项重点时什么上下文下才能调用这段核心代码。

2.1 内核模块里获取当前进程和父进程的cmdline的核心代码

这里面其实分为两个子任务,一个是获取当前进程的父进程的pid,另一个是根据pid获取pid对应的cmdline。

为什么要这么分?因为获取当前进程的pid和其父进程的pid是没有上下文环境限制的,当前进程的pid非常简单,只需要current->pid即可,当前进程的父进程的pid的获取相对复杂一些,只需要加rcu的锁保护,确保current的real_parent变量在获取real_parent变量(real_parent变量是一个task_struct指针,指向task_struct表示的线程的父进程,一个线程通过getppid得到的是父进程的pid,而不是父进程里创建该子进程的线程id,这个会在2.3一节里做实验验证,但是如果通过real_parent->pid得到的是父进程里创建该子进程的线程id,这个会在2.4一节里做实验验证)在访问其pid信息时是有效的(在访问期间没被释放)。

而获取线程/进程的cmdline则是有上下文环境限制的,因为获取一个进程的cmdline并没有获取一个进程的comm那么简单(获取一个进程的comm直接从task_struct里的comm[16]数组里拷贝走就行了),获取一个进程的cmdline的获取期间需要加锁,需要放在可睡眠的上下文环境里,如kworker里。

2.1.1 获取当前进程的父进程的pid和comm的逻辑

先看代码:

void get_parent_pid_and_comm(struct output_items* io_pitems, struct task_struct* i_ptask)
    rcu_read_lock();
    parent = rcu_dereference(i_ptask->real_parent);
    io_pitems->currppid = parent->pid;
    strlcpy(io_pitems->currppidcomm, parent->comm, TASK_COMM_LEN);
    rcu_read_unlock();
}

从上面代码可以看到,需要用rcu锁来保护,在引用rcu保护的变量时,要用rcu_dereference去引用指针,然后再去获取指针指向的内容。关于rcu的细节见第三章及后面的博文。

关于real_parent的细节见下面的 2.1.1.1 一节。

2.1.1.1 关于task_struct的real_parent指针

首先如下图,task_struct里的real_parent是一个task_struct的指针,且受到rcu的保护:

关于ppid等父进程的信息的获取,需要使用real_parent变量,而不是其他变量,可以参考getppid系统调用的实现,如下:

我们实现的方式和系统调用getppid实现方式还是有一点区别,getppid系统调用用的是父进程id,我们直接用real_parent->pid是父进程里创建子进程的那个线程的线程id。getppid的例子验证在2.3一节,我们的real_parent->pid的例子验证在2.4一节。

不管是哪种,rcu锁还是要保护,使用real_parent这个指针也是一样。

关于real_parent这个受rcu锁保护的变量,在父进程退出以后,会进行刷新动作,如果不做prctl(PR_SET_CHILD_SUBREAPER)的动作的话,刷新后的real_parent指向到了systemd(pid是1),这个会在2.5一节里做实验,而做prctl(PR_SET_CHILD_SUBREAPER)的动作的话,刷新后的real_parent指向到了做prctl(PR_SET_CHILD_SUBREAPER)的进程,这个会在2.6一节里做实验。

下面,我把上面描述的相关的内核代码的部分贴出来:

上图中通过find_new_reaper函数找到了子收尸者,然后一一线程进行real_parent指针的重新assign,这里用的是RCU_INIT_POINTER这个宏,这个宏相对于常用的rcu_assign_pointer来赋值,它有性能优势,但是要使用起来非常小心,细节可以看到内核里的RCU_INIT_POINTER的宏的注释,这里就不展开了。

关于find_new_reaper函数是如何找子收尸者的,见下图,可以从注释里看到一些逻辑上的思路细节:

2.1.2 获取线程/进程的cmdline的逻辑

先看一下核心逻辑代码:

分为几个部分,先是根据pid找到对用的struct pid,通过struct pid获取task_struct的指针,这里面find_get_pid会增加pid结构体的引用计数,所以需要调用put_pid来释放。另外get_pid_task会增加task_struct的引用计数,所以在用完后,要通过put_task_struct来释放。下面代码里有一个my_set_cmdline函数,这个函数马上会介绍。

int pid_to_get_cmdline;
struct task_struct* ptask;
struct pid* pid_struct;
char temp_commandline[128];

pid_struct = find_get_pid(pid_to_get_cmdline);
if (pid_struct) {
    ptask = get_pid_task(pid_struct, PIDTYPE_PID);
    if (ptask) {
        my_set_cmdline(temp_commandline, 128, ptask);
        put_task_struct(ptask);
    }
    else {
        temp_commandline[0] = '\0';
    }
    put_pid(pid_struct);
}
else {
    temp_commandline[0] = '\0';
}

my_set_cmdline函数的实现:

这个my_set_cmdline函数是调用了my_get_cmdline获取到cmdline的原始数据,但是cmdline原始数据内容每个arg参数的最后一个字节是\0,我们要把cmdline作为完整的字符串记到别的地方方便显示的话,要把这些参数最后的\0替换成空格,当然最后一个\0不能替换成空格。

void my_replace_null_with_space(char *str, int n) {
    for (int i = 0; i < n - 1; i++) {
        if (str[i] == '\0') {
            str[i] = ' ';
        }
    }
}

void my_set_cmdline(char* i_pbuff, int i_buffsize, struct task_struct* i_ptask)
{
    int ret = my_get_cmdline(i_ptask, i_pbuff, i_buffsize);
    if (ret <= 0) {
        i_pbuff[0] = '\0';
        return;
    }
    my_replace_null_with_space(i_pbuff, ret);
    i_pbuff[ret - 1] = '\0';
}

下面我们来看一下核心逻辑里的核心my_get_cmdline函数:

int my_get_cmdline(struct task_struct *task, char *buffer, int buflen)
{
	int res = 0;
	unsigned int len;
	struct mm_struct *mm = get_task_mm(task);
	unsigned long arg_start, arg_end, env_start, env_end;
	if (!mm)
		goto out;
	if (!mm->arg_end)
		goto out_mm;	/* Shh! No looking before we're done */

	spin_lock(&mm->arg_lock);
	arg_start = mm->arg_start;
	arg_end = mm->arg_end;
	env_start = mm->env_start;
	env_end = mm->env_end;
	spin_unlock(&mm->arg_lock);

	len = arg_end - arg_start;

	if (len > buflen)
		len = buflen;

	res = access_process_vm(task, arg_start, buffer, len, FOLL_FORCE);

	/*
	 * If the nul at the end of args has been overwritten, then
	 * assume application is using setproctitle(3).
	 */
	if (res > 0 && buffer[res-1] != '\0' && len < buflen) {
		len = strnlen(buffer, res);
		if (len < res) {
			res = len;
		} else {
			len = env_end - env_start;
			if (len > buflen - res)
				len = buflen - res;
			res += access_process_vm(task, env_start,
						 buffer+res, len,
						 FOLL_FORCE);
			res = strnlen(buffer, res);
		}
	}
out_mm:
	mmput(mm);
out:
	return res;
}

上面这段代码,其实内核里有这个函数的实现,只是这个函数并不作为export symbol给外部模块使用,我们拷贝了一份到我们模块里,这个函数里的主要函数access_process_vm是export symbol的,所以拿过来用没有什么问题。

参考的内核源码里的函数是(mm/util.c里get_cmdline函数):

需要注意,相对早一些的内容,需要多包含linux/sched/mm.h这个头文件,下面的是我当前这个用到nmi和sched的tracepoint以及interrupt和workqueue和hrtimer的这些的头文件集合,供参考:

#include <linux/module.h>
#include <linux/capability.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/ctype.h>
#include <linux/seq_file.h>
#include <linux/poll.h>
#include <linux/types.h>
#include <linux/ioctl.h>
#include <linux/errno.h>
#include <linux/stddef.h>
#include <linux/lockdep.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/wait.h>
#include <linux/init.h>
#include <asm/atomic.h>
#include <trace/events/workqueue.h>
#include <linux/sched/clock.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/interrupt.h>
#include <linux/tracepoint.h>
#include <trace/events/osmonitor.h>
#include <trace/events/sched.h>
#include <trace/events/irq.h>
#include <trace/events/kmem.h>
#include <linux/ptrace.h>
#include <linux/uaccess.h>
#include <asm/processor.h>
#include <linux/sched/task_stack.h>
#include <linux/nmi.h>
#include <asm/apic.h>
#include <linux/version.h>
#include <linux/sched/mm.h>

我们回到内核里的get_cmdline这个函数,看看这个函数是怎么拿到任意一个task_struct的cmdline的。

由于cmdline内容是属于具体进程上下文内容部分的,需要借助具体进程上下文的vm_area_struct信息:

get_cmdline用到了access_process_vm,access_process_vm里用到了__access_remote_vm,__access_remote_vm用到了vm_area_struct和get_user_page_vma_remote函数:

而get_user_page_vma_remote其实就是调用的之前的博客 里讲到get_user_pages_remote函数,关于get_user_pages_remote和get_user_pages/pin_user_pages 参考之前的博客 非gdb方式观察应用程序的运行时的变量状态_程序运行变量监控-CSDN博客 和 内存管理之——get_user_pages和pin_user_pages及缺页异常-CSDN博客。

要注意,从get_cmdline里设的是FOLL_FORCE作为gup_flags传下来,在__access_remote_vm里带上了FOLL_WRITE参数,一块传给get_user_page_vma_remote里,虽然和get_user_pages和pin_user_pages一样最终调用了__get_user_pages,但是和get_user_pages和pin_user_pages不一样的是,get_user_pages和pin_user_pages会带上能锁住内存的FOLL_GET或FOLL_PIN标志位,而这里的__access_remote_vm里和非gdb方式观察应用程序的运行时的变量状态_程序运行变量监控-CSDN博客 博客里讲到的get_user_pages_remote函数都不会带上这两个锁内存的标志位来传给最终调用的__get_user_pages核心函数。具体关于锁内存相关内容,见 内存管理相关——malloc,mmap,mlock与unevictable列表_mlock内存可以迁移吗-CSDN博客 和 内存管理之——get_user_pages和pin_user_pages及缺页异常-CSDN博客。

2.2 什么上下文下才能调用这段核心代码

其实在2.1里也提及了部分原因,这里再强调一下,分了两个部分,获取当前进程的pid和父进程的pid是可以在任何上下文,包括中断里甚至nmi中断里。而获取一个进程的cmdline则不能在硬中断里执行,因为获取逻辑里可能会涉及缺页异常,导致中断里套中断;另外,获取逻辑会使用spin_lock,非spin_lock_irqsave,其运行期间会被硬中断打断,就算用了spin_lock_irqsave也还是会被nmi打断,所以放到硬中断里容易发生spinlock死锁,稍有不慎就panic或卡死。所以,获取进程的cmdline这段逻辑,必须运行在可睡眠的上下文里,推荐的就是运行在kworker里。

2.3 一个子进程通过getppid得到的是其父进程的pid而不是父进程里创建该子进程的线程id

关于这个,做一下实验验证:

实验代码:

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

// 获取线程 ID 的宏
#define gettid() syscall(SYS_gettid)

// 线程函数
void *thread_function(void *arg) {
    printf("Process ID: %ld\n", getpid());
    printf("Thread ID: %ld\n", gettid());

    // 创建子进程
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        return NULL;
    }

    if (pid == 0) {
        // 子进程
        printf("In child process:\n");
        printf("Child PID: %d\n", getpid());
        printf("Child TID: %ld\n", gettid());
        printf("Parent PID: %d\n", getppid());
        exit(0); // 子进程结束
    } else {
        // 父进程
        wait(NULL); // 等待子进程结束
    }

    return NULL;
}

请用gcc来编译,用g++编译会找不到wait函数

运行后的结果:

可以看到进程105597里创建了一个线程105598,在这个105598线程里,创建了一个子进程105599,子进程里读取自己的parent pid,用的是getppid()这个函数,得到的是父进程的pid,而不是父进程里创建该子进程的线程id

这里,普及一个基础:

用户态的pid是表示进程id,用户态的tid是表示线程id

内核态的pid是表示线程id,内核态的tgid是表示当前线程所在的进程的线程id,也就是thread group id,即tgid

2.4 通过real_parent->pid得到的是父进程里创建该子进程的线程id

借助2.3的程序,在退出前增加一个while(1)死循环,然后,在insmod的内核模块的sched_stat_runtime里判断是子进程的pid就打印其real_parent的pid和tgid,来做确认。

先加上while(1)死循环逻辑,然后编译运行:

在sched_stat_runtime的注册的tracepoint回调里编写判断和打印逻辑(关于tracepoint如何添加自定义注册和内核模块里添加自定义注册的方法参考之前的博客 内核模块注册调度的tracepoint的回调,逻辑里判断当前线程处于内核态还是用户态的方法-CSDN博客 内核tracepoint的注册回调及添加的方法_tracepoint 自定义回调-CSDN博客):

dmesg里的输出如下:

这个结果其实也印证了在2.1.1.1一节里提到的getppid系统调用里的实现:

上图中用的是task_tgid_vnr,注意里面包含了tgid的字样

2.5 默认情况下,在父进程退出以后,子进程的real_parent会被指向到pid是1的systemd进程

所谓默认情况,就是不做prctl(PR_SET_CHILD_SUBREAPER)的动作。关于做prctl(PR_SET_CHILD_SUBREAPER)的实验,我们在2.6一节里做实验

下面的实验是在2.4的程序的基础上再做一下改动来实验:

把testppid.c的父进程的wait动作去掉:

cb_sched_stat_runtime里的改动:

dmesg的输出信息:

对于用户态里进行getppid获取也一样,sleep(10)以后获取的结果如下:

如果通过pstree -p | grep <pid>去查看进程父子链关系,可以看到其实如果父进程不退出的话,它的父子链是很长的

2.6 prctl(PR_SET_CHILD_SUBREAPER)设置当前进程为子“收尸者”,即child_subreaper

相关的代码逻辑在2.1.1.1里已涉及。

这里直接做实验,代码如下:

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

int main() {
    // 设置当前进程为子收尸者
    if (prctl(PR_SET_CHILD_SUBREAPER, 1) < 0) {
        perror("prctl(PR_SET_CHILD_SUBREAPER) failed");
        return 1;
    }

    // 创建父进程
    pid_t parent_pid = fork();
    if (parent_pid < 0) {
        perror("fork failed");
        return 1;
    }

    if (parent_pid == 0) {
        // 这是父进程
        // 创建子进程
        pid_t child_pid = fork();
        if (child_pid < 0) {
            perror("fork failed");
            return 1;
        }

        if (child_pid == 0) {
            // 这是子进程
            sleep(2);
            printf("Child process (PID: %d)(ppid: %d) is exiting...\n", getpid(), getppid());
            exit(0); // 子进程正常退出
        } else {
            // 父进程
            printf("Parent process (PID: %d) is exiting...\n", getpid());
            exit(0); // 父进程正常退出
        }

    } else {
        // 这是祖父进程
        printf("Grandparent process (PID: %d) is waiting for child...\n", getpid());
        
        // 等待子进程结束
        {
            int status;
            while(wait(&status) > 0); // 祖父进程等待子进程结束
        }
        printf("Grandparent process reaped the child process!\n");
    }

    return 0;
}

注意:上面的代码里在子进程退出前,我sleep了2秒,然后打印了getppid的值,即父进程的pid

实验的运行结果:

可以看到在父进程退出后,子进程child process的ppid并没有和2.5一节里的实验一样变成systemd(pid是1),而是变成了调用过prctl(PR_SET_CHILD_SUBREAPER)的祖父进程。

三、给内核的高性能神器rcu的介绍起一个头

内核的rcu锁来自于rwlock的进一步演变,rcu锁和rwlock锁都是针对读多写少的情形,但是rwlock在一些极端情况下,比如多个读者同时进行读操作,因为涉及到cas的add操作,会导致性能因为缓存一致性导致降低得很厉害,而rcu锁如果只有读者的情况下并不需要额外的针对数据和数据相关的管理的开销,并不会标记一些脏数据而导致缓存一致性mesi协议导致的性能开销。

下图是一张浓缩了一系列相关概念精华的思维导图截图,在后面的博文中会详细展开介绍:

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

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

相关文章

京东OPPO定制版 12.1.0 | 安装包只有4.7M,无短视频,适合轻度使用

底部四个标签&#xff0c;没有短视频&#xff0c;可以正常登录。安装包体积很小&#xff0c;功能基本可用&#xff0c;特别适合追求简洁界面和轻度使用的用户。 大小&#xff1a;4.7M 下载地址&#xff1a; 百度网盘&#xff1a;https://pan.baidu.com/s/1lD0o1y9X3s4hRiz-8F…

AOSP的同步问题

repo sync同步时提示出错: error: .repo/manifests/: contains uncommitted changesRepo command failed due to the following UpdateManifestError errors: contains uncommitted changes解决方法&#xff1a; 1、cd 进入.repo/manifests cd .repo/manifests2、执行如下三…

Redis【1】- 如何阅读Redis 源码

1 Redis 的简介 Redis 实际上是简称&#xff0c;全称为 Remote Dictionary Server (远程字典服务器)&#xff0c;由 Salvatore Sanfilippo 写的高性能 key-value 存储系统&#xff0c;其完全开源免费&#xff0c;遵守 BSD 协议。Redis 与其他 key-value 缓存产品&#xff08;如…

鸿蒙修饰符

文章目录 一、引言1.1 什么是修饰符1.2 修饰符在鸿蒙开发中的重要性1.3 修饰符的作用机制 二、UI装饰类修饰符2.1 Styles修饰符2.1.1 基本概念和使用场景2.1.2 使用示例2.1.3 最佳实践 2.2 Extend修饰符2.2.1 基本概念2.2.2 使用示例2.2.3 Extend vs Styles 对比2.2.4 使用建议…

nginx安装和负载均衡

1. nginx安装 &#xff08;1&#xff09;安装依赖项&#xff1a; yum -y install gcc gcc-c make libtool zlib zlib-devel openssl openssl-devel pcre pcre-devel&#xff08;2&#xff09;下载Nginx源代码&#xff1a; http://nginx.org/en/download.html https://nginx.o…

部署 Prometheus

实验环境 IP地址服务192.168.88.10Prometheus服务端, Consul, Grafana, Node-Exporter192.168.88.77MySQL, Node-Exporter192.168.88.30Nginx&#xff0c;Node-Exporter 一、Prometheus Server 端安装和相关配置 【Prometheus1.sh】 &#xff08;1&#xff09;上传 prometh…

通过深度点图表示的隐式场实现肺树结构的高效解剖标注文献速递-生成式模型与transformer在医学影像中的应用

Title 题目 Efficient anatomical labeling of pulmonary tree structures via deeppoint-graph representation-based implicit fields 通过深度点图表示的隐式场实现肺树结构的高效解剖标注 01 文献速递介绍 近年来&#xff0c;肺部疾病&#xff08;Decramer等&#xff…

# 22_ Python基础到实战一飞冲天(二)-python基础(二十二)--名片管理系统案例:cards_tools.py文件

22_ Python基础到实战一飞冲天&#xff08;二&#xff09;-python基础&#xff08;二十二&#xff09;–名片管理系统案例&#xff1a;cards_tools.py文件 一、框架搭建-09-准备名片操作函数修改主文件中函数调用 1、名片管理系统 案例&#xff1a;框架搭建 — cards_tools.p…

Python 和 Pyecharts 对Taptap相关数据可视化分析

结果展示&#xff1a; 数据来源&#xff1a; Python爬取TapTap 热门游戏信息并存储到数据库&#xff08;详细版&#xff09; 目录 结果展示&#xff1a; 数据来源&#xff1a; Python爬取TapTap 热门游戏信息并存储到数据库&#xff08;详细版 一、引言 二、准备工作 三、…

泷羽sec-shell (3)脚本参数传递与数学运算

声明&#xff01; 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团队无关&a…

重塑视频新语言,让每一帧都焕发新生——Video-Retalking,开启数字人沉浸式交流新纪元!

模型简介 Video-Retalking 模型是一种基于深度学习的视频再谈话技术&#xff0c;它通过分析视频中的音频和图像信息&#xff0c;实现视频角色口型、表情乃至肢体动作的精准控制与合成。这一技术的实现依赖于强大的技术架构和核心算法&#xff0c;特别是生成对抗网络&#xff0…

Mybatis Plus 增删改查方法(一、增)

先定义一个简单的测试表&#xff0c;执行脚本如下&#xff1a; create table user(id bigint primary key auto_increment,name varchar(255) not null,age int not null default 0 check (age > 0) ); 根据Spingbootmybatisplus的结构根据表自行构建结构&#xff0c;大致…

依赖倒置原则:Java实践篇

在软件开发的世界里&#xff0c;设计原则如同指南针&#xff0c;指引着我们构建更加健壮、可维护和可扩展的系统。其中&#xff0c;依赖倒置原则&#xff08;Dependency Inversion Principle&#xff0c;DIP&#xff09;是面向对象设计&#xff08;OOD&#xff09;中的一个重要…

【MySQL】库和表的基本操作

目录 库 库的增删查改 字符集与校验集 库的备份与恢复 表 表的创建和删除 用不同的存储引擎创建表的区别 查看表 修改表 添加删除属性 修改改变属性 上篇博客我们讲了数据库的基本理解&#xff0c;对数据库有了一个大致的概念&#xff0c;下面我们来介绍一下库和表的…

大数据新视界 -- 大数据大厂之 Hive 函数库:丰富函数助力数据处理(上)(11/ 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

03.ES7 04.ES8

3.1.Array.includes Includes 方法用来检测数组中是否包含某个元素&#xff0c;返回布尔类型值 <script>// includes const mingzhu [王二,张三,李四,王五];//判断console.log(mingzhu.includes(张三));//trueconsole.log(mingzhu.includes(周六));//false//indexOf …

中国科学院大学研究生学术英语读写教程 Unit7 Materials Science TextA 原文和翻译

中国科学院大学研究生学术英语读写教程 Unit7 Materials Science TextA 原文和翻译 Why Is the Story of Materials Really the Story of Civilisation? 为什么材料的故事实际上就是文明的故事&#xff1f; Mark Miodownik 1 Everything is made of something. Take away co…

下载安装Android Studio

&#xff08;一&#xff09;Android Studio下载地址 https://developer.android.google.cn/studio 滑动到 点击下载文档 打开新网页 切换到english ![](https://i-blog.csdnimg.cn/direct/b7052b434f9d4418b9d56c66cdd59fae.png 等待一会&#xff0c;出现 点同意后&#xff0…

【解决方案】pycharm出现 为项目选择的Python解释器无效

文章目录 1.问题重述2.解决方案END 1.问题重述 第二次启动项目的时候出现 2.解决方案 右下角点 先选无解释器&#xff0c;然后在用项目配置好的解释器&#xff0c;然后就好了&#xff0c;估计是第二次启动的时候没有识别到&#xff0c;UI的信号设置的问题 END

浏览器的数据六种存储方法比较 :LocalStorage vs. IndexedDB vs. Cookies vs. OPFS vs. WASM-SQLite

在构建该 Web 应用程序&#xff0c;并且希望将数据存储在用户浏览器中。也许您只需要存储一些小标志&#xff0c;或者甚至需要一个成熟的数据库。 我们构建的 Web 应用程序类型发生了显着变化。在网络发展的早期&#xff0c;我们提供静态 html 文件。然后我们提供动态渲染的 h…