实验15.多线程调度

news2024/11/27 18:44:59

简介

实验.多线程调度

内核线程

    1.在时钟中断函数中处理中,减少当前线程pcb的tick,tick为0则启动调度
    2.调度,把当前线程pcb放入就绪对立队尾,把就绪线程队首拿出来执行

主要代码

引导

省略

内核

list.h

#ifndef __LIB_KERNEL_LIST_H
#define __LIB_KERNEL_LIST_H

#include "global.h"
#include "stdint.h"

/// 拿到menber相当于struct_type的偏移
/// struct_type 结构体
// member 结构体的属性
#define offset(struct_type, member) (int)(&((struct_type*)0)->member)

///  返回这个属性地址的结构体地址
/// struct_type 结构体
/// struct_member_name 结构体属性
/// elem_ptr 结构体的属性的地址
#define elem2entry(struct_type, struct_member_name, elem_ptr) \
    (struct_type*)((int)elem_ptr - offset(struct_type, struct_member_name))

/// @brief 链表元素
struct list_elem {
    struct list_elem* prev;  // 前躯元素
    struct list_elem* next;  // 后继元素
};

/// @brief 双向链表
struct list {
    // 这两个就如同两堵墙,在墙中间的才是元素
    struct list_elem head;  // 队首 注意: 第一个元素是head.next!!!
    struct list_elem tail;  // 队尾
};

/// @brief 自定义函数类型function
typedef bool(function)(struct list_elem*, int arg);

void list_init(struct list*);
void list_insert_before(struct list_elem* before, struct list_elem* elem);
void list_push(struct list* plist, struct list_elem* elem);
void list_iterate(struct list* plist);
void list_append(struct list* plist, struct list_elem* elem);
void list_remove(struct list_elem* pelem);
struct list_elem* list_pop(struct list* plist);
bool list_empty(struct list* plist);
uint32_t list_len(struct list* plist);
struct list_elem* list_traversal(struct list* plist, function func, int arg);
bool elem_find(struct list* plist, struct list_elem* obj_elem);
#endif

list.c

#include "list.h"
#include "interrupt.h"

/// @brief 初始化双向链表
/// @param list 链表指针
void list_init(struct list* list) {
    list->head.prev = NULL;
    list->head.next = &list->tail;
    list->tail.prev = &list->head;
    list->tail.next = NULL;
}

/// @brief 把链表元素elem插入在元素before之前
/// @param before 链表元素
/// @param elem  链表元素
void list_insert_before(struct list_elem* before, struct list_elem* elem) {
    // 关闭中断
    enum intr_status old_status = intr_disable();

    // 将before前驱元素的后继元素更新为elem, 暂时使before脱离链表
    before->prev->next = elem;
    // 更新elem自己的前驱结点为before的前驱
    elem->prev = before->prev;
    // 更新elem自己的后继结点为before, 于是before又回到链表
    elem->next = before;
    // 更新before的前驱结点为elem
    before->prev = elem;

    // 恢复原来的中断状态
    intr_set_status(old_status);
}

/// @brief 添加元素到列表队首,类似栈push操作
/// @param plist 队列
/// @param elem 元素
void list_push(struct list* plist, struct list_elem* elem) {
    list_insert_before(plist->head.next, elem);
}

/// @brief 追加元素到链表队尾
/// @param plist
/// @param elem
void list_append(struct list* plist, struct list_elem* elem) {
    list_insert_before(&plist->tail, elem);
}

/// @brief 使元素pelem脱离链表
/// @param pelem 元素
void list_remove(struct list_elem* pelem) {
    // 关闭中断
    enum intr_status old_status = intr_disable();

    pelem->prev->next = pelem->next;
    pelem->next->prev = pelem->prev;

    // 恢复中断状态
    intr_set_status(old_status);
}

/// @brief 将链表第一个元素弹出并返回,类似栈的pop操作
/// @param list 双向链表
struct list_elem* list_pop(struct list* plist) {
    struct list_elem* elem = plist->head.next;
    list_remove(elem);
    return elem;
}

/// @brief 从链表中查找元素obj_elem,
/// @param plist
/// @param obj_elem
/// @return 成功时返回true,失败时返回false
bool elem_find(struct list* plist, struct list_elem* obj_elem) {
    struct list_elem* elem = plist->head.next;
    while (elem != &plist->tail) {
        if (elem == obj_elem) { return true; }
        elem = elem->next;
    }
    return false;
}

/// @brief 遍历列表内所有元素,逐个判断是否有符合条件的元素
/// @param list 链表
/// @param function 判断函数,返回true位满足条件,false位不满足条件
/// @param int 参数个数
struct list_elem* list_traversal(struct list* plist, function func, int arg) {
    struct list_elem* elem = plist->head.next;
    if (list_empty(plist)) return NULL;

    while (elem != &plist->tail) {
        if (func(elem, arg)) {  // func返回ture则认为该元素在回调函数中符合条件,直接返回
            return elem;
        }
        elem = elem->next;
    }
    return NULL;
}

/// @brief 返回链表长度
/// @param plist 链表
/// @return 链表长度
uint32_t list_len(struct list* plist) {
    struct list_elem* elem = plist->head.next;
    uint32_t length = 0;
    while (elem != &plist->tail) {
        length++;
        elem = elem->next;
    }
    return length;
}

/// @brief 判断链表是否为空
/// @param plist 链表
/// @return 空时返回true,否则返回false
bool list_empty(struct list* plist) {  // 判断队列是否为空
    return (plist->head.next == &plist->tail ? true : false);
}

switch.s

; switch.s
; 时间: 2024-07-25
; 来自: ccj
; 描述: 定义切换pcb的函数

;---切换pcb(cur_pcb,next_pcb) begin---
section .text
global switch_to
switch_to:
    ; 保存当前环境
    push esi
    push edi
    push ebx
    push ebp

    ; 保存当前环境到cur_pcb
    mov eax, [esp + 20] ; 得到栈中的参数cur_pcb, cur_pcb = [esp+20]
    mov [eax], esp      ; 保存esp到pcb的 self_kstack =
                        ; 结果 curpub.self_kstack = esp

    ; 切换当前环境到nex_pcb
    mov eax, [esp + 24] ; 得到栈中的参数next_pcb, next_pcb = [esp+24]
    mov esp, [eax]      ; esp = next_pcb.self_kstack

    ; 恢复当前环境
    pop ebp
    pop ebx
    pop edi
    pop esi

    ret
;---切换pcb(cur_pcb,next_pcb) end---

thread.h

#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"

typedef void thread_func(void*);

/// @brief 进程或线程的状态
enum task_status {
    TASK_RUNNING,  //
    TASK_READY,
    TASK_BLOCKED,
    TASK_WAITING,
    TASK_HANGING,
    TASK_DIED
};

/// @brief 中断栈,中断发生时,保护程序(线程或进程)的上下文环境
/// 进程或线程被外部中断或软中断打断时,会按照此结构压入上下文寄存器
/// kernel.s中intr_exit的出栈操作是此结构的逆操作
/// 中断栈在pcb的最顶端
struct intr_stack {
    // kernel.s 宏VECTOR中push %1压入的中断号
    uint32_t vec_no;

    // kernel.s 中pushad压入的寄存器
    uint32_t edi;
    uint32_t esi;
    uint32_t ebp;
    uint32_t esp_dummy;  // 虽然pushad把esp也压入,但esp是不断变化的,所以会被popad忽略
    uint32_t ebx;
    uint32_t edx;
    uint32_t ecx;
    uint32_t eax;

    // kernel.s 中保存上下文环境压入的寄存器
    uint32_t gs;
    uint32_t fs;
    uint32_t es;
    uint32_t ds;

    // 以下由cpu从低特权级进入高特权级时压入
    uint32_t err_code;  // err_code会被压入在eip之后
    void (*eip)(void);
    uint32_t cs;
    uint32_t eflags;
    void* esp;
    uint32_t ss;
};

/// @brief 线程栈 保存线程的上下文
/// ABI规则:主调函数调用被调函数,被调函数一定要保存ebp、ebx、edi、esi、esp
/// eip:线程下一步
struct thread_stack {
    uint32_t ebp;
    uint32_t ebx;
    uint32_t edi;
    uint32_t esi;

    // 线程第一次执行时,eip指向待调用的函数kernel_thread
    // 其它时候,eip是指向switch_to的返回地址
    void (*eip)(thread_func* func, void* func_arg);

    // 以下仅供第一次被调度上cpu时使用
    void(*unused_retaddr);  // 占位置充数为返回地址
    thread_func* function;  // 由Kernel_thread所调用的函数名
    void* func_arg;         // 由Kernel_thread所调用的函数所需的参数
};

/// @brief 进程或线程的pcb process control block 4096字节
/// 一个pcb包含1个中断栈,1个线程栈,
struct task_struct {
    uint32_t* self_kstack;    // pcb中线程栈的地址
    enum task_status status;  // 状态
    uint8_t priority;         // 线程优先级
    char name[16];            //
    uint32_t stack_magic;     // pcb魔数,用于检测栈的溢出
};

void thread_create(struct task_struct* pthread, thread_func function, void* func_arg);
void init_thread(struct task_struct* pthread, char* name, int prio);
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg);
#endif

thread.c

// 文件: thread.c
// 时间: 2024-07-23
// 来自: ccj
// 描述: 申请了一个物理页做pcb,在pcb中定义了下一步kernel_thread,然后切换到该pcb的环境,运行

#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"

#define PG_SIZE 4096

/// @brief 由kernel_thread去执行function(func_arg)
/// @param function 函数指针
/// @param func_arg 函数参数
static void kernel_thread(thread_func* function, void* func_arg) { function(func_arg); }

/// @brief 初始化pcb,将待执行的函数和参数放到pcb中相应的位置
/// @param pthread pcb
/// @param function 待执行的函数
/// @param func_arg 函数参数
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
    // 先预留中断使用栈的空间,可见thread.h中定义的结构
    pthread->self_kstack -= sizeof(struct intr_stack);

    // 再留出线程栈空间
    pthread->self_kstack -= sizeof(struct thread_stack);
    // 此时的self_kstack看作线程栈的首地址
    struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;
    kthread_stack->eip = kernel_thread;  // 指向kernel_thread
    kthread_stack->function = function;  // 设置函数
    kthread_stack->func_arg = func_arg;  // 设置参数
    kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}

/// @brief 初始化线程基本信息
/// @param pthread pcb
/// @param name 线程名
/// @param prio 优先级
void init_thread(struct task_struct* pthread, char* name, int prio) {
    // 清0
    memset(pthread, 0, sizeof(*pthread));

    // 设置名字、状态、优先级
    strcpy(pthread->name, name);
    pthread->status = TASK_RUNNING;
    pthread->priority = prio;

    // self_kstack是线程自己在内核态下使用的栈顶地址
    pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
    pthread->stack_magic = 0x19870916;  // 自定义的魔数
}

/// @brief 创建一优先级为prio的线程,线程名为name,线程所执行的函数是function(func_arg)
/// @param name 线程名
/// @param prio 优先级
/// @param function 线程要执行的函数
/// @param void* 函数参数
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) {
    // 申请1个物理页来存放pcb
    struct task_struct* thread = get_kernel_pages(1);

    // 初始化pcb
    init_thread(thread, name, prio);

    // 创建线程
    thread_create(thread, function, func_arg);

    // 切换到thread的上下文,并执行thread
    asm volatile("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret"
                 :
                 : "g"(thread->self_kstack)
                 : "memory");
    // movl %0, %%esp;
    // 把thread->self_kstack的值给esp

    // pop %%ebp; pop %%ebx; pop %%edi; pop %%esi;
    // 从thread->self_kstack中把这几个属性值给到寄存器

    // ret
    // 由于堆栈和这些关键寄存器已被设置为新线程的值,这会导致执行流跳转到新线程的上下文中继续执行
    return thread;
}

}

/// @brief 初始化线程环境
/// @param
void thread_init(void) {
    put_str("[thread] thread_init start\n");

    list_init(&thread_ready_list);
    list_init(&thread_all_list);

    // 将当前main函数创建为线程
    make_main_thread();
    put_str("[thread] thread_init done\n");
}

timer.h

#ifndef __DEVICE_TIME_H
#define __DEVICE_TIME_H

#include "stdint.h"

void timer_init(void);
#endif

timer.c

// 文件: timer.c
// 时间: 2024-07-23
// 来自: ccj
// 描述: 调快时钟,调快时钟、注册时钟中断来调度线程

#include "timer.h"
#include "io.h"
#include "print.h"
#include "interrupt.h"
#include "thread.h"
#include "debug.h"

#define IRQ0_FREQUENCY   100
#define INPUT_FREQUENCY  1193180
#define COUNTER0_VALUE   INPUT_FREQUENCY / IRQ0_FREQUENCY
#define CONTRER0_PORT    0x40
#define COUNTER0_NO      0
#define COUNTER_MODE     2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43

uint32_t ticks;  // 中断开始,开始计数

/* 把操作的计数器counter_no、读写锁属性rwl、计数器模式counter_mode写入模式控制寄存器并赋予初始值counter_value
 */
static void frequency_set(uint8_t counter_port,
                          uint8_t counter_no,
                          uint8_t rwl,
                          uint8_t counter_mode,
                          uint16_t counter_value) {
    /* 往控制字寄存器端口0x43中写入控制字 */
    outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
    /* 先写入counter_value的低8位 */
    outb(counter_port, (uint8_t)counter_value);
    /* 再写入counter_value的高8位 */
    outb(counter_port, (uint8_t)counter_value >> 8);
}

/* 时钟的中断处理函数 */
static void intr_timer_handler(void) {
    struct task_struct* cur_thread = running_thread();

    ASSERT(cur_thread->stack_magic == 0x19870916);  // 检查栈是否溢出

    cur_thread->elapsed_ticks++;  // 记录此线程占用的cpu时间嘀
    ticks++;                      // 记录总时钟数

    if (cur_thread->ticks == 0) {  // 若进程时间片用完就开始调度新的进程上cpu
        schedule();
    } else {  // 将当前进程的时间片-1
        cur_thread->ticks--;
    }
}

/* 初始化PIT8253 */
void timer_init(void) {
    put_str("[timer] timer_init start\n");

    /* 设置8253的定时周期,也就是发中断的周期 */
    frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);

    // 注册时钟中断,用来调度线程
    register_handler(0x20, intr_timer_handler);
    put_str("[timer] timer_init done\n");
}

interupt.h

#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H

#include "stdint.h"

typedef void* intr_handler;

void idt_init(void);

/// @brief 中断机制状态
enum intr_status {
    INTR_OFF,  // 中断关闭
    INTR_ON    // 中断打开
};
enum intr_status intr_enable(void);
enum intr_status intr_disable(void);
enum intr_status intr_get_status(void);
enum intr_status intr_set_status(enum intr_status);
void register_handler(uint8_t vector_no, intr_handler function);

#endif

interupt.c

// 文件: timer.c
// 时间: 2024-07-23
// 来自: ccj
// 描述: 调快时钟,调快时钟、注册时钟中断来调度线程

#include "timer.h"
#include "io.h"
#include "print.h"
#include "interrupt.h"
#include "thread.h"
#include "debug.h"

#define IRQ0_FREQUENCY   100
#define INPUT_FREQUENCY  1193180
#define COUNTER0_VALUE   INPUT_FREQUENCY / IRQ0_FREQUENCY
#define CONTRER0_PORT    0x40
#define COUNTER0_NO      0
#define COUNTER_MODE     2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43

uint32_t ticks;  // 中断开始,开始计数

/* 把操作的计数器counter_no、读写锁属性rwl、计数器模式counter_mode写入模式控制寄存器并赋予初始值counter_value
 */
static void frequency_set(uint8_t counter_port,
                          uint8_t counter_no,
                          uint8_t rwl,
                          uint8_t counter_mode,
                          uint16_t counter_value) {
    /* 往控制字寄存器端口0x43中写入控制字 */
    outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
    /* 先写入counter_value的低8位 */
    outb(counter_port, (uint8_t)counter_value);
    /* 再写入counter_value的高8位 */
    outb(counter_port, (uint8_t)counter_value >> 8);
}

/* 时钟的中断处理函数 */
static void intr_timer_handler(void) {
    struct task_struct* cur_thread = running_thread();

    ASSERT(cur_thread->stack_magic == 0x19870916);  // 检查栈是否溢出

    cur_thread->elapsed_ticks++;  // 记录此线程占用的cpu时间嘀
    ticks++;                      // 记录总时钟数

    if (cur_thread->ticks == 0) {  // 若进程时间片用完就开始调度新的进程上cpu
        schedule();
    } else {  // 将当前进程的时间片-1
        cur_thread->ticks--;
    }
}

/* 初始化PIT8253 */
void timer_init(void) {
    put_str("[timer] timer_init start\n");

    /* 设置8253的定时周期,也就是发中断的周期 */
    frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);

    // 注册时钟中断,用来调度线程
    register_handler(0x20, intr_timer_handler);
    put_str("[timer] timer_init done\n");
}

init.h

#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif

init.c

// 文件: init.c
// 时间: 2024-07-22
// 来自: ccj
// 描述: 内核所有初始化操作

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"

/// @brief 内核所有初始化
void init_all() {
    put_str("init all\n");

    idt_init();    // 初始化中断
    timer_init();  // 调快时钟、注册时钟中断来调度线程
    mem_init();  // 初始化内存管理系统
    thread_init();
}

main.c

// 文件: main.c
// 时间: 2024-07-19
// 来自: ccj
// 描述: 内核从此处开始

#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"

void k_thread_a(void*);
void k_thread_b(void*);

int main(void) {
    put_str("I am kernel\n");

    init_all();

    thread_start("k_thread_a", 4, k_thread_a, "argA\n");
    thread_start("k_thread_a", 16, k_thread_b, "argB\n");

    intr_enable();  // 打开中断,使时钟中断起作用
    while (1) { put_str("Main\n"); };
    return 0;
}

void k_thread_a(void* arg) {
    char* para = arg;
    while (1) { put_str(para); }
}

void k_thread_b(void* arg) {
    char* para = arg;
    while (1) { put_str(para); }
}

编译

省略

运行

start.sh

#/bin/bash
# 文件: start.sh
# 描述: 启动bochs
# 时间: 2024-07-19
# 来自: ccj


set -x

bin/bochs -f bochsrc.disk

在这里插入图片描述

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

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

相关文章

【GoodERP更新日志】增加采购发票、销售发票 批量抵扣记账 批量撤销入账 功能

开源项目GoodERP更新-2024年7月29日 本次提交合并增加的功能或解决的问题&#xff1a; 1、增加采购发票、销售发票 批量抵扣记账 批量撤销入账 功能&#xff08;增加上了批量抵扣记账&#xff08;会检查发票号、开票日期有没有填写上&#xff09;、批量撤销入账 两个批量功能…

H616设计时候存在的问题

1.存在大量孤铜的问题&#xff1a; 这种情况是绝对不允许的&#xff0c;但是GBA焊盘打大量的过孔会出现很多这样的孤铜&#xff1a; 解决办法&#xff1a; 像这种出现大量重复焊盘的&#xff0c;用导线连接起来&#xff0c;之后铺铜形成铜皮&#xff0c;再在这个小铜皮上面打…

用frp内网穿透https网站

场景说明 在微信小程序上线测试的时候&#xff0c;自主开发的后端服务在公司局域网&#xff0c;小程序前端在微信公众平台只支持配置https协议的域名来访问服务端。公司一直在使用frp内网穿透工具实现公网访问公司局域网服务&#xff0c;因此&#xff0c;研究如何实现frp代理h…

低代码平台飞书apaas

1. 低代码平台 1.1 概述 低代码是无需编码&#xff08;0 代码&#xff09;或通过少量代码就可以快速生成应用程序的开发平台。 通过可视化进行应用程序开发的方法&#xff0c;具有不同经验水平的开发人员可以通过图形化的用户界面&#xff0c;使用拖拽组件和模型驱动的逻辑来…

BIM、数字孪生、可视化一结合,我就知道这大屏效果稳稳的啦

在日常的项目中&#xff0c;经常会用到上述三个方面的能力结合&#xff0c;比如智慧工地、智慧楼宇、智慧园区等项目&#xff0c;本文就分享一批这方面的精彩作品。 BIM&#xff08;建筑信息模型&#xff09;、数字孪生和可视化大屏可以结合起来&#xff0c;为建筑行业和工程管…

C语言中的二维数组

文章目录 &#x1f34a;自我介绍&#x1f34a;二维数组&#x1f34a;代码实战 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以&#xff1a;点赞关注评论收藏&#xff08;一键四连&#xff09;哦~ &#x1f34a;自我介绍 Hello,大家好&#xff0c;我是小珑也要变强&…

AI 绘画是否符合当代主流审美?

在当今时代&#xff0c;AI 绘画成为了一个备受关注的热门话题。那么&#xff0c;AI 绘画是否符合当代主流审美呢&#xff1f; AI 绘画有着诸多符合当代审美的特质。它能展现出独特的视觉效果&#xff0c;风格丰富多样&#xff0c;如后现代风格、奇幻风格等等&#xff0c;足以满…

前端必备基础【网络通信】(2024最新版)

Ajax Asynchronous Javascript and XML 的缩写&#xff0c;是使用 JS 发起网络通信的技术统称&#xff0c;具体步骤为&#xff1a; 创建 XMLHttpRequest 实例发出 HTTP 请求接收服务器传回的数据更新网页数据&#xff08;通常是部分内容&#xff0c;而不是整个网页&#xff09…

B端:导航条就框架提供的默认样式吗?非也,看过来。

导航条不一定必须使用框架提供的默认样式&#xff0c;你可以根据项目需求和设计风格进行自定义。通过使用框架提供的自定义选项、CSS样式覆盖、自行设计或者使用其他UI库或组件&#xff0c;你可以实现独特且符合需求的导航条样式。 下面发一些参考给友友们&#xff0c;可以让设…

请你谈谈:vue的渲染机制(render)- 2举例说明问题

如何在 Vue 的 render 函数中使用 createElement 方法来创建虚拟节点&#xff08;VNode&#xff09;。这里是一个稍微整理后的示例&#xff0c;它直接对应于你提供的注释和代码片段&#xff0c;但作为一个完整的 render 函数的一部分&#xff0c;可能位于一个 Vue 组件的 scrip…

关于ITSS认证-IT服务工程师、IT服务项目经理常见问题解答!

TSS&#xff0c;即信息技术服务标准&#xff08;Information Technology Service Standards&#xff0c;简称ITSS&#xff09;&#xff0c;是一套系统化的信息技术服务规范。 它全面规定了信息技术服务产品及其组成要素&#xff0c;旨在指导标准化的信息技术服务实施&#xff…

企业微信开发智能升级:AIGC技术赋能,打造高效沟通平台

文章目录 一、AIGC在企业微信开发中的核心价值1. 智能化客服体验2. 自动化工作流程3. 个性化内容推荐4. 深度数据分析与洞察 二、使用AIGC进行企业微信开发的实践路径1. 需求分析与场景定义2. 技术选型与平台搭建3. 模型训练与调优4. 接口对接与功能集成5. 测试与优化 《企业微…

css各种使用案例合集(一)

1、文字不换行 场景1&#xff1a;使 div 标签的文字内容不换行 代码示例&#xff1a; <div class"nowrap-div">这是一段很长的文字&#xff0c;我们不会让它换行。</div> .nowrap-div { white-space: nowrap; } 2、步骤条 场景2&#xff1a;特殊样式的…

【模拟电路】电与磁的关系,电感与震荡电路

文章目录 前言电与磁的关系电感及其应用电感的电路符号 震荡电路及其作用震荡电路的画法 总结 前言 在电子技术的世界中&#xff0c;电与磁的关系是理解许多电路和设备工作原理的基础。电与磁不仅在理论上紧密相连&#xff0c;在实际应用中也发挥着重要作用。电感器和震荡电路…

新学年即将到来,IT管理员面临一系列挑战

新学年即将到来&#xff0c;学校的IT管理员们又将迎来一场风暴般的挑战。你知道吗&#xff1f;许多教育机构在管理学生账户和访问权限方面都面临着巨大的困难。随着数字化学习的普及&#xff0c;简化和安全的账户管理需求比以往任何时候都更加迫切。本文将详细探讨学生账户管理…

java使用hutool工具判断ip或者域名是否可用,java使用ping判断ip或者域名是否可用

1.导入hutool工具 <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version></dependency>2.复制以下代码直接运行 import cn.hutool.core.net.NetUtil;public class Test {p…

python爬虫的基础知识

1.学习爬虫的好处 提升编程技能&#xff1a;爬虫开发需要掌握编程基础&#xff0c;特别是网络请求、HTML/CSS/JavaScript解析、数据存储和异常处理等技能。通过学习爬虫&#xff0c;你可以巩固和提升你的编程技能&#xff0c;特别是Python等编程语言的应用能力。 数据驱动决策…

LitCTF2024赛后web复现

复现要求&#xff1a;看wp做一遍&#xff0c;自己做一遍&#xff0c;第二天再做一遍。&#xff08;一眼看出来就跳过&#xff09; 目录 [LitCTF 2024]浏览器也能套娃&#xff1f; [LitCTF 2024]一个....池子&#xff1f; [LitCTF 2024]高亮主题(划掉)背景查看器 [LitCTF 2…

VRT: 关于视频修复的模型(论文复现)

VRT: 关于视频修复的模型&#xff08;论文复现&#xff09; 本文所涉及所有资源均在传知代码平台可获取 文章目录 VRT: 关于视频修复的模型&#xff08;论文复现&#xff09;2. 视频修复概述3. 现有方法的挑战与局限性4. VRT模型详解5. 实验与结果6. VRT的优势与创新点7. 实际应…

Git基本原理讲解、常见命令、Git版本回退、Git抛弃本地分支拉取仓库最新分支、如何将本地文件推送至github、.gitignore文件的使用

借此机会写篇博客汇总一下自己去公司实习之后遇到的一些常见关于Git的操作。 Git基本认识 Git把数据看作是对小型文件系统的一组快照&#xff0c;每次提交更新&#xff0c;或在Git中保存项目状态时&#xff0c;Git主要对当时的全部文件制作一个快照并保存这个快照的索引。同时…