Linux入门——11 线程

news2024/11/15 23:22:44

线程的概念,线程的控制,线程的同步和互斥,队列结构,线程池,锁

1.预备知识

1.1可重入函数

1.1.1链表的头插

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。

main执行流和信号捕捉方法执行流同时发生,就会出现两个执行流,

结论:1.一般而言,我们认为:main执行流和信号捕捉执行流是两个执行流

2.如果在main中,和handler中,该函数被重复进入,出问题,该函数被称为-----不可重入函数

3.如果在main中,和handler中,该函数被重复进入,没有出问题,该函数被称为-----可重入函数

***************我们目前用到的接口都是不可重入的。

函数可不可以重入是特性,是一个中性词!

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

1.2 volatile(保持内存可见性)


#include <stdio.h>
#include <signal.h>

int quit = 0;

void handler(int signo)
{
    printf("%d号信号正在被捕捉\n",signo);
    printf("quit: %d",quit);
    quit = 1;
    printf(" -> %d\n",quit);
}

int main(int argc, char const *argv[])
{
    signal(2,handler);
    while(!quit);
    printf("我是正常退出的\n");
    return 0;
}

cpu主要做三件事情,取指令,分析指令,执行指令

正常情况下在main执行流中,更改quit的值是在内存中改的,但是进行优化后,quit被放到寄存器中,这时如果执行信号捕捉动作流的时候,改的还是内存的数据,但寄存器中的值还是0,没有被改,所以main执行流还是使用寄存器中的quit.

所以为了解决这类问题,我们要使用关键字volatile

让每一次读取quit的值,不从寄存器中读取,而是从内存中读取。

 2.什么是线程

在创建进程的时候会有虚拟内存,虚拟内存里面决定了进程能够看到的资源(代码区,堆区,栈区。。。),我们通过地址空间和页表就能访问代码所需要的资源,

一个进程是可以把自己的划分出一部分,让另一样执行流去执行,如fork(),让父子进程分别执行不同的代码块。也可以发生写实拷贝。

先在我们让再次创建的进程不再有独立的地址空间,而是共享父进程的地址空间,就相当于一个房间,有5,6个人一起通过窗户看,每个人都可以通过同一个窗户看,让每一个进程访问我代码中的一部分,访问一部分资源

类似这种,只创建pcb(struct_task)不分配地址空间的,从父进程中分配资源的方法叫做线程。

因为我们可以通过虚拟地址空间+页表方式对进程进行资源划分,单个“进程”执行力度,一定要比之前的进程要细

  • 站在CPU的角度,会如何看待一个一个的PCB(task_struct)呢?

CPU不会管你有没有虚拟地址空间。它只认识task_struct,只会对每一个PCB进行计算

  • 如果OS要专门创建设计“线程”概念,OS要不要进程管理?

可能需要,如果有,如何管理呢?

先描述在组织

一定要对线程设计专门的数据结构对象(TCB),常见的Windows系统就是专门这样做的

线程创建的本质就是为了被执行。被调度(id,状态,优先级,上下文,栈。。。。);

这时会发现,线程和进程有很多的地方是重叠的,

所以我们的Linux工程师,我们不想给Linux系统专门设置线程的数据结构,我们直接用PCB来表示Linux下的“线程”。

Linux下的线程,就是在OS内创建pcb,然后指向父线程的地址空间,通过页表给线程分配一些资源

结论:线程在进程内运行,线程在进程地址空间内运行!拥有进程的一部分资源。

进程的概念:内核视角下,承担分配系统资源的基本实体(创建进程时候,申请一个地址空间,创建一个pcb和一堆的页表和加载到物理内存的代码和数据,所有这些消耗的资源,我们叫做进程,以前讲的进程内部只有一个执行流)

线程的概念:CPU调度的基本单位!

以前是一个pcb和地址空间页表内存中的代码和数据,叫做进程

现在是一堆的PCB和地址空间页表内存中的代码和数据,叫做进程

以前讲的进程内部只有一个执行流,今天讲的是一个进程内部可以有多个执行流,

站在CPU角度,以前我们讲的是CPU调度的是一个进程,今天,CPU调度的是进程中的一个分支(执行流)

今天我们喂给CPU的task_struct

  • Linux内核中有没有真正意义的线程呢?

1.严格意义上是没有的,Linux利用进程PCB模拟线程的,是一种完全属于自己的一套方案。

2.站在CPU视角,每一个PCB,都可以称为轻量级进程

3.Linux线程是CPU调度的基本单位。而进程是承担分配资源的基本单位

4.进程是用来整体申请资源,线程是伸手向进程要资源。

5.Linux中没有真正意义上的线程。没有线程之名但有线程之实

6.好处是:可以服用PCB的调度算法,并不用维护线程与进程的关系,不用做数据结构之间的耦合,让编码上更简单,维护成本低------可靠高效

7.缺点是:OS、程序员只认线程,Linux无法直接提供线程的系统调用接口,而只能给我们提供轻量级进程的接口!(去银行打不了饭)

后续如果资源不足,线程需要资源,OS还是会给的,只是本质上是进程在要。

2.1pthread_create(线程创建库函数)

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

参数:

pthread_t *thread无符号整数的线程id

const pthread_attr_t *attr ,不要管,全设为NULL

void *(*start_routine) (void *) 函数指针,让创建的线程执行这个函数(回调函数)

void *arg 回调函数,在执行时使用的参数

返回值:

成功返回0,失败返回错误码

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <unistd.h>
#include <cstdio>

using namespace std;


//新线程
void* thread_routine(void* args)
{
    const char* name = (const char*)args;
    while(true)
    {
        cout<<"我是新线程,我正在运行,name:" << name << std::endl;
        sleep(1);
    }
    
}

int main(int argc, char const *argv[])
{
    pthread_t tid;  //tid地址
    int n = pthread_create(&tid,nullptr,thread_routine,(void*)"thread_on");
    assert(0 == n);
    (void)n;


    //主线程
    while (true)
    {
        char tidbuffer[64];  
        snprintf(tidbuffer,sizeof(tidbuffer),"0x%x",(unsigned int)tid);
        cout<<"我是主线程,我正在运行,我创建出来的线程tid:"<< tidbuffer << endl;
        sleep(1);
    }
    


    return 0;
}

需要使用命令ps -aL查看轻量级进程

有两个执行流,且pid是一样的。说明这两个是属于同一个进程的,但是LWP是不一样的,LWP(light weight process)轻量级进程ID

但PID和LWP是一样的轻量级线程为主线程。

CPU调度是以LWP为标识符表示特定的一个执行流的。

以前我们理解的是PID,是因为用的都是单线程,PID==LWP

pthread_create函数得到的tid是一个地址,与系统看到的LWP是并不一样的。

2.1线程一旦被创建,几乎所有资源都是被所有线程共享的

 

所有的线程都能调用定义的函数

线程之间的数据的很方便共享

但是不是所有的数据都共享,只是大部分的数据是共享的。

线程也一定有自己的私有内部属性

什么资源是线程私有的?

1.PCB属性私有,

2.要有一定的私有上下文结构

3.每一个线程都要有自己的独立的栈结构

3.错误号

5.优先级

与进程之间的切换相比,进程之间的切换需要操作系统需要OS做的工作少很多。

1. 进程:切换页表 && PCB &&上下文 && 虚拟地址空间

2. 线程:切换PCB && 上下文数据

3.线程切换cache不用太更新,但是进程切换,就要全部更新

CPU中除了有寄存器还有cache(硬件级缓存),对数据的保存的功能,CPU在访问数据的时候,可以不访问内存,直接访问cache.如果cache没有命中,cache从内存中去读取,再让CPU来读取。

一个进程它的内部已经缓存了许多热点数据!

如果是进程,这里面的数据都要进行切换,而如果是线程的话,这里面的数据就不用被切换。

线程的缺点:

性能的损失:线程数和核数最好是一样的,比如CPU是单核的,现在是3个线程,线程也要进行切换。

CPU的多核:CPU中有运算器和控制器,多核可以理解为CPU中存在多个运算器

多CPU就是多个独立的CPU

健壮性降低:一个线程出问题,可能会影响多个线程

缺乏访问控制:全局变量的访问控制

编程调试困难

线程的优点:

创建新线程的代价要比创建有一个新进程的代价小的多

线程之间的切换不进程之间的切换,系统做的工作要少很多

线程占 的资源要少的多

能够充分使用处理器完成并发的工作

在等待慢速IO过程中,程序可执行其他操作

计算密集型应用能够在多处理器上运行

io密集型应用,为了提高性能,将io操作重叠

线程共享的资源

除了pcb,内存地址空间,页表,

还有文件描述符表,每种信号的处理方式是共享的,当前工作目录,用户id和组id

线程私有资源

线程ID

寄存器(上下文)

ernno

信号屏蔽字

调度优先级

3.线程的健壮性问题

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>

using namespace std;

void* start_routine(void* argv);


int main(int argc, char const *argv[])
{
    pthread_t id;
    pthread_create(&id,nullptr,start_routine,(void*)"start_routine");
    while (true)
    {
        cout << "main thread running" <<endl;
        sleep(1);
    }
    
    return 0;
}

void* start_routine(void* argv)
{  
    string name = static_cast<const char*>(argv);   //安全的强制类型转换并检查
    while (true)
    {
        cout << "new thread:" << name <<endl;
        sleep(1);
    }
    int *p =nullptr;
    // p = nullptr;   //p本来就是空指针
    *p = 0; //这个会报错,直接对nullptr解引用是不行的,本质是向0号地址内写0
    
}

这里 *p = 0,野指针错误。会影响整个进程

一个线程如果出现问题,会影响其他线程,这可以说是健壮性或鲁棒性差

为什么呢?

之前讲的信号是进程信号,发送给整个进程的,所有线程的pid都是想等的,所以OS向所有的同一个pid号进程发送信号会导致整个所有线程全部结束

一个进程包括地址空间,页表和内存中对应的代码和结构,多个或一个执行流

一个线程是进程的一部分,如果线程出了问题,就相当于进程出现了问题。 进程要被释放,所有依附这个进程的线程都会被释放

4.线程和进程的关系

5.clone线程创建调用库,但依旧是要系统提供接口

fork底层也是调用这个接口,

clone,生成进程或者轻量级进程

int clone(int (*fn)(void *), void *child_stack,int flags, void arg, .../ pid_t *ptid, void *newtls, pid_t *ctid */ );

参数

        int (*fn)(void *) //新执行流要执行的代码

        void *child_stack //子栈

vfork也可以创建子线程,不过与fork不同的是,创建出来的线程与父线程,共享地址空间,也就是轻量级进程

6.线程控制

6.1线程库

#POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  • 要使用这些函数库,要通过引入头文
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

6.2 pthread_create创建线程

功能:创建一个新的线程

原型

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);

参数

        thread:返回线程ID

        attr:设置线程的属性,attr为NULL表示使用默认属性

        start_routine:是个函数地址,线程启动后要执行的函数

        arg:传给线程启动函数的参数

返回值:

成功返回0;失败返回错误码

错误检查:

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小

6.3线程的终止

6.3.1线程函数结束,return的时候,线程就终止了

6.3.2pthread_exit(void *retval);线程终止函数

以前我们学到exit(0);函数不能用于终止线程,会让整个进程直接终止。

---------》任何一个执行流调用exit都会让整个进程结束任务

void pthread_exit(void *retval);

参数:void *retval

这个参数和如何获得这个参数,通过线程等待之后解决

 6.3.3pthread_cancel线程取消

线程是可以被取消的,线程被取消,前提是线程已经跑起来了。

int pthread_cancel(pthread_t thread);

参数:线程号;

线程如果是被取消的,它的退出码就是-1,本质是一个宏PTHREAD_CANCELED;

线程必须有阻塞点(sleep)才能被取消。

如果没有取消点,可以手动添加

void pthread_testcancel(void);

有的线程可以取消,有的线程不可以取消,可以使用,但是如果不让取消,一定要保证状态修改这句代码已经执行,否者会直接取消。

int pthread_setcancelstate(int state, int *oldstate);

state:

PTHREAD_CANCEL_ENABLE //可以被取消

PTHREAD_CANCEL_DISABLE //不可以被取消

例如前5秒不能被取消,后面可以被取消

还可以设置取消类型

int pthread_setcanceltype(int type, int *oldtype);

PTHREAD_CANCEL_DEFERRED 等到取消点才取消(默认)

PTHREAD_CANCEL_ASYNCHRONOUS 目标线程会立即取消

6.2.4pthread_clean_push线程的清理

如果 线程在取消前申请了内存没有释放,就会浪费资源,这时候,就需要线程清理

void pthread_cleanup_push(void (*routine) (void *), void *arg)

void pthread_cleanup_pop(int execute)

这两个函数必须成对使用

没有成对使用报的错

正确使用

pthread_cleanup_pop的参数如果是0,就不会在执行pthread_cleanup_push里面的回调函数了

如果pthread_cleanup_pop的参数如果是非0,就会直接去执行pthread_cleanup_push里面的回调函数

这个pthread_cleanup_push里面的回调函数被执行的条件

1. 被pthraead_cancel取消掉

2.执行pthread_exit

3.非0参数执行pthread_cleanup_pop

线程中的return可以直接结束线程,但是不能触发pthread_cleanup_push里面的回调函数

6.3.5pthread_self(),获取子线程tid

 7.线程的等待

线程也是需要等待的,如果不等待会发生什么问题呢?

如果不等待,也会照成类似僵尸进程的问题-----内存泄露。

等待的目的:

  • 获取回收新线程的退出信息------》可以不关心,但是不能没有
  • 回收新线程对应的PCB等内核资源,防止内存泄露-------暂时无法查看!

等待的方法:int pthread_join(pthread_t thread, void **retval);

7.1pthread_join()线程等待函数

int pthread_join(pthread_t thread, void **retval);

参数:

        pthread_t thread //线程id

        void **retval //void pthread_exit(void *retval);

返回值:

成功返回0,失败返回错误码

//多线程的等待
    for(auto &iter : threads)
    {
        pthread_join(iter->tid,nullptr);
    }

7.2线程的返回值问题

无论是pthread_exit还是pthread_join函数,都让我们传入参数返回值

针对int pthread_join(pthread_t thread, void **retval);中的void ** retval是输出型参数,用来获取线程函数结束时,返回的退出结果!!!

线程返回值结果是void*,要想将其输出就要使用void**

类型是什么:

我身上有100,我身上有多少钱?100元,100美元,100分、、、、、

你并不知道

现在返回值如果是return (void*)106;

代表的就是返回的是地址,只是这个地址里面写的是106

在我们自己的代码空间中,定义了一个变量,void*ret;而(void*)106是指针。

我们的线程退出的时候,会将退出结果保存到pthread库中。

pthread_joint的本质是从库中调取指定线程的退出信息

现在如何获取这个退出信息到我们定义的变量中呢?

在你的空间中定义一个指针变量。由于是个变量,可以将指针变量的地址传进去,

&ret

*(&ret) 就等于库中的这个变量,

把库中的变量(void*)ret拷贝到&ret中,再解引用,就是这个数本身,就相当于直接将这个数拷贝到ret中

 7.3线程退出的信号

线程出异常,整个进程都会退出。

//ptread_join默认就会调用成功,不考虑异常问题,异常问题是你进程考虑的问题。

7.4分离线程。如果线程不进行等待

线程不存在非阻塞等待,要么等,要么不等。

分离线程

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

线程可以自己分离自己,也可以由父进程进行分离

7.4.1pthread_detach线程分离函数

 

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstdio>

std::string changeId(const pthread_t &threadid)
{
    char tid[128];
    snprintf(tid,sizeof(tid),"ox%x",(unsigned int)threadid);  //自己获取自己的线程id
    return tid;
}


void* start_routine(void* args)
{
    std::string thread_name = static_cast<const char*>(args);
    pthread_detach(pthread_self()); //自己把自己设置为分离状态
    while(true)
    {
        
        std::cout << "name:" << thread_name << "running...new thread id:"<< changeId(pthread_self()) << std::endl;
        sleep(1);
    }
}




int main(int argc, char const *argv[])
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread_1");
    std::string main_id = changeId(pthread_self());
    std::cout <<  "main running...main thread id:"<< main_id << "   new thread id:"<< changeId(tid) << std::endl;

    pthread_join(tid,nullptr);
    return 0;
}

一个线程默认是jointable的,如果设置了分离状态就不能再分离了。

7.4.2新线程自己分离自己

 pthread_self首先要获得自己的线程id

谁调用这个函数,就返回这个线程的id

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>

std::string changeId(const pthread_t &threadid)
{
    char tid[128];
    snprintf(tid,sizeof(tid),"ox%x",(unsigned int)threadid);  //自己获取自己的线程id
    return tid;
}


void* start_routine(void* args)
{
    std::string thread_name = static_cast<const char*>(args);
    pthread_detach(pthread_self()); //自己把自己设置为分离状态
    int cnt = 5;
    while(cnt--)
    {
        
        std::cout << "name:" << thread_name << "running...new thread id:"<< changeId(pthread_self()) << std::endl;
        sleep(1);
    }
}




int main(int argc, char const *argv[])
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread_1");
    std::string main_id = changeId(pthread_self());
    std::cout <<  "main running...main thread id:"<< main_id << "   new thread id:"<< changeId(tid) << std::endl;

    int n = pthread_join(tid,nullptr);
    std::cout<<"result " << n << ":" << strerror(n) << std::endl;
    return 0;
}

std::cout

无论线程是否被分离,int n = pthread_join(tid,nullptr);中的n都是0,success,为什么呢?

新线程和主线程,创建号后谁先运行?

不确定,如果新线程还没执行pthread_detach,主线程直接join,就直接进入阻塞等待了。不管你后面有没有分离

这是有问题的。

一个线程被join的时候,一定要保证已经被分离了。可以在之前先sleep一下。

这种做法,不太合理,还是推荐由主线程直接将新线程分离。

 7.4.3主线程分离新线程

#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>

std::string changeId(const pthread_t &threadid)
{
    char tid[128];
    snprintf(tid,sizeof(tid),"ox%x",(unsigned int)threadid);  //自己获取自己的线程id
    return tid;
}


void* start_routine(void* args)
{
    std::string thread_name = static_cast<const char*>(args);
    //pthread_detach(pthread_self()); //自己把自己设置为分离状态
    int cnt = 5;
    while(cnt--)
    {
        
        std::cout << "name:" << thread_name << "running...new thread id:"<< changeId(pthread_self()) << std::endl;
        sleep(1);
    }
}


int main(int argc, char const *argv[])
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread_1");
    pthread_detach(pthread_self()); //主线程在创建好新线程的时候,直接将其设置为分离状态
    std::string main_id = changeId(pthread_self());
    std::cout <<  "main running...main thread id:"<< main_id << "   new thread id:"<< changeId(tid) << std::endl;
    sleep(2);
    int n = pthread_join(tid,nullptr);
    std::cout<<"result " << n << ":" << strerror(n) << std::endl;
    return 0;
}

7.4.4pthread_attr_t attr; /*通过线程属性来设置游离态(分离态)*/

设置线程属性为分离

pthread_attr_t attr;

pthread_attr_init(&attr);

pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

pthread_create(&tid,&attr,func,NULL);

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

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

相关文章

续——网络通信编程

一、网络通信 1、编程 &#xff08;1&#xff09;基于UDP c/s通信模型 -------server——服务端——被动角色------- socket 全双工的&#xff08;可读可写&#xff09;。同上篇。 bind int bind(int sockfd , struct sockaddr *my_addr&#xff08;所绑定的地址信息&…

Linux的进程详解(进程创建函数fork和vfork的区别,资源回收函数wait,进程的状态(孤儿进程,僵尸进程),加载进程函数popen)

目录 什么是进程 Linux下操作进程的相关命令 进程的状态&#xff08;生老病死&#xff09; 创建进程系统api介绍&#xff1a; fork() 父进程和子进程的区别 vfork() 进程的状态补充&#xff1a; 孤儿进程 僵尸进程 回收进程资源api介绍&#xff1a; wait() waitpid…

编译运行 llama.cpp (vulkan, Intel GPU SYCL)

llama.cpp 是一个运行 AI (神经网络) 语言大模型的推理程序, 支持多种 后端 (backend), 也就是不同的具体的运行方式, 比如 CPU 运行, GPU 运行等. 但是编译运行 llama.cpp 并不是那么容易的, 特别是对于 SYCL 后端 (用于 Intel GPU), 坑那是一大堆. 只有特定版本的 llama.cpp…

【代码随想录训练营第42期 Day38打卡 - 动态规划Part6 - LeetCode 322. 零钱兑换 279.完全平方数 139.单词拆分

目录 一、做题心得 二、题目与题解 题目一&#xff1a;322. 零钱兑换 题目链接 题解&#xff1a;动态规划--完全背包 题目二&#xff1a; 279.完全平方数 题目链接 题解&#xff1a;动态规划--完全背包 题目三&#xff1a;139.单词拆分 题目链接 题解&#xff1a;动…

blender骨骼绑定(让物体动起来)

园哥摸索了两天了&#xff0c;骨骼做好就是不能带动物体&#xff0c;点击时候要选中那个骨骼点圆圈&#xff0c;点中间骨骼没用。终于动起来了。虽然有点奇怪。 点击图二那个点&#xff0c;貌似我的骨骼生长反了。做游戏是真麻烦。本来想搞个简单的2d游戏&#xff0c;结果那个瓦…

一起学Java(4)-[起步篇]教你掌握本协作项目中的Gralde相关配置文件(上)

将思绪拉回java-all-in-one项目&#xff0c;如果你fork并下载了代码&#xff0c;你会看到在项目中除了HelloWorldMain代码外&#xff0c;还存在很多文件。如果你并不了解他们的作用并有足够的好奇心&#xff0c;那你应该想要知道他们的作用。带着好奇&#xff0c;今天我也来研究…

网络抓包测试

利用fgets遇到\n停止的特性&#xff0c;给流数据直接加间隔&#xff0c;fgets读的时候会把soket缓冲区里面的数据全部放到fgets的缓冲区内&#xff0c;再读的时候就不能从套接字fd描述符读而是从fgets的fq里面读 行为1. 读取行为&#xff1a;•fgets 读取字符直到遇到换行符 \n…

下载ncurses操作步骤

https://invisible-island.net/ncurses/announce.htmlncurses-6.5.官网下载链接 选择下载版本

信刻离线文件单向导入系统

信刻针对不同数据单向导入的需求&#xff0c;按需推出的离线文件单向导入系统采用软硬件相结合的技术&#xff0c;支持信息导入申请、身份认证、光盘读取、病毒查杀、光盘复刻、光盘数据信息导入、审查审批、用户管理、日志管理、三权管理、数据加密、数据检查、校验、安全审计…

pd虚拟机 Parallels Desktop 19 for Mac安装教程【支持Intel和M芯片】

pd虚拟机 Parallels Desktop 19 for Mac安装教程【支持Intel和M芯片】 一、准备工作 二、开始安装 安装包内有三个软件 Parallels Desktop是一款广受好评的Mac虚拟机软件&#xff0c;本文来讲述一下Parallels Desktop是如何下载、安装、激活与换机的。 Parallels Desktop 下…

外排序之文件归并排序实现

外排序介绍 外排序是指能够处理极大量数据的排序算法。通常来说&#xff0c;外排序处理的数据不能一次装入内存&#xff0c;只能放在读写较慢的外存储器(通常是硬盘)上。外排序通常采用的是⼀种“排序-归并”的策略。在排序阶段&#xff0c;先读入能放在内存中的数据量&#x…

【Kafka源码走读】消息生产者与服务端的连接过程

说明&#xff1a;以下描述的源码都是基于最新版&#xff0c;老版本可能会有所不同。 一. 查找源码入口 kafka-console-producer.sh是消息生产者的脚本&#xff0c;我们从这里入手&#xff0c;可以看到源码的入口&#xff1a; if [ "x$KAFKA_HEAP_OPTS" "x&qu…

Vue处理表格长字段显示问题

背景 有些单元个中会有比较长的内容&#xff0c;如果使用默认格式&#xff0c;会导致单元格的高度比较怪异&#xff0c;需要将超长的内容进行省略。 当前效果&#xff1a; 优化效果&#xff1a; 优化代码&#xff1a; 在内容多的单元格增加下面代码 <el-table-columnprop…

SAP成本核算-事前控制(标准成本核算)

一、BOM清单 1、BOM清单抬头 BOM用途:决定成本核算控制的依据 物料清单状态:决定成本核算控制的依据 基本数量:用于计算标准的用量 有效期:决定生产工单开单的日期范围,以及成本核算的日期范围 物料清单状态默认值后台配置:事务代码OS21 2、BOM清单行项目 有效期:决…

Java框架Shiro、漏洞工具利用、复现以及流量特征分析

Shiro流量分析 前言 博客主页&#xff1a; 靶场&#xff1a;Vulfocus 漏洞威胁分析平台 Shiro&#xff08;Apache Shiro&#xff09;是一个强大且灵活的开源安全框架&#xff0c;专为Java应用程序提供安全性解决方案。它由Apache基金会开发和维护&#xff0c;广泛应用于企业级…

Anolis os系统进入单用户模式重置密码

Anolis os系统进入单用户模式重置密码 1、重启计算机。 2、在启动时&#xff0c;当GRUB菜单出现时&#xff0c;按下任意键停止自动倒计时。 3、选择要启动的内核版本&#xff0c;然后按e键编辑启动参数。 4、找到以linux或linux16开头的行&#xff0c;通常这行包含了启动内核…

keepalived与lvs

1 lvs Linux服务器集群系统(一) -- LVS项目介绍 LVS&#xff08;Linux Virtual Server&#xff09;即Linux虚拟服务器,是一个基于Linux操作系统的虚拟服务器技术&#xff0c;用于实现负载均衡和高可用性。章文嵩&#xff0c;是中国国内最早出现的自由软件项目之一。 2 lvs发展…

Circuitjs 快捷键完全列表

对于常见组件, 反复通过菜单去选择也是比较繁琐的, 系统考虑到这一点, 也为那些常用组件添加了快捷键. 通过 菜单--选项--快捷键... 可以查看所有快捷键, 分配新的快捷键或调整现有的快捷键. 点开菜单时, 位于菜单右侧的那些字母即是对应的快捷键, 如下图所示: 注: 旧版本有, …

Debug-022-el-upload照片上传-整体实现回顾

前情概要&#xff1a; 最近业务里通过el-upload实现一个上传图片的功能&#xff0c;有一些小小的心得分享给各位。一方面是review一下&#xff0c;毕竟实现了很多细小的功能&#xff0c;还有这么多属性、方法&#xff08;钩子&#xff09;和碰到的问题&#xff0c;感觉小有成就…

Swing中如何实现快捷键绑定和修改

在许许多多市面上常见的桌面软件中, 可以使用快捷键操作&#xff0c; 比如像微信一样,使用AltA 可以打开截图窗口&#xff0c;如果不习惯于AltA按键时&#xff0c;还可以打开设置去修改。 如果在swing中也想实现一个快捷键绑定和修改的操作&#xff0c;那么应该如何操作&#…