【Linux】进程控制:父进程与子进程

news2025/4/6 18:59:04

目录

  • 1 进程创建
    • 1.1 认识fork
    • 1.2 进程创建的目的
    • 1.3 写时拷贝
    • 1.4 进程创建失败的场景
  • 2 进程退出
    • 2.1 进程退出状态
    • 2.2 进程退出的方式
    • 2.3 exit、_exit、return
      • 2.3.1 概念
      • 2.3.2 区别
  • 3 进程等待
    • 3.1 理解进程等待
    • 3.2 进程等待的方式
      • 3.2.1 wait和waitpid
      • 3.2.2 status位图结构
    • 3.3 阻塞等待和非阻塞等待
  • 4 进程替换
    • 4.1 进程替换的原理
    • 4.2 进程替换的函数
      • 4.2.1 认识exec函数
      • 4.2.2 exec函数的命名理解
  • 5. 制作一个简易的Shell


1 进程创建

💭 在上一篇文章《进程的学习 —— Linux下的进程》中,频繁用到了fork来创建子进程。没错,fork正是Linux中创建进程的一个系统调用接口,下面将更深入地剖析fork的用法、作用、原理及特点。

1.1 认识fork

  • fork是什么?

在linux中fork函数是非常重要的系统调用接口,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

  • fork的工作机制?

🔎调用fork后,进程执行内核空间的fork函数代码,内核会做出如下操作:

  1. 开辟新的内存空间、创建新的进程控制块,供新进程使用
  2. 将父进程PCB数据结构部分拷贝至子进程中
  3. 添加子进程到系统进程列表中
  4. fork返回,调度器开始调度

fork前后执行流程图

在这里插入图片描述

  • fork怎么用?
#include <unistd.h> // 包含头文件
pid_t fork(void); // 函数声明
  • fork的返回值问题

fork的返回值比较特殊,它是一个pid_t(可以视为是整型)类型的变量,若父进程创建子进程成功,fork给父进程返回子进程pid,给子进程返回0。若创建失败,则给父进程返回-1。

man手册中关于fork返回值的介绍


RETURN VALUE
       On success, the PID of the child process is returned in the parent, and 0 is returned in the child.  
       On failure, -1 is returned in theparent, no child process is created, and errno is set appropriately.

❓这里比较奇怪的是,为什么一个函数会有两个返回值呢❓

💡 fork在内核中也有属于自己的代码,那么fork函数内部,在return之前,肯定是已经创建完子进程并且分两个执行流了,父进程执行流会返回子进程的pid,子进程执行流会返回0。(不考虑创建进程失败的情况)

在这里插入图片描述


1.2 进程创建的目的

💭 创建进程的目的一般有如下两种:

  1. 父进程希望生成一份自己的副本,执行同一个程序中不同的代码片段。
  2. 让子进程执行不同的程序。(涉及进程替换,后文详细分析)

1.3 写时拷贝

📝父子进程的数据是共享的,在父子进程都没有对共享数据进行修改之前,这些数据对于父子进程来说都处于相同的地址(虚拟地址和物理地址)。当父子有任一方对共享数据做出修改时,就会发生写时拷贝。
写时拷贝的具体操作:OS在物理空间上开辟一块新的空间,并将欲修改数据拷贝过去,修改数据方对应的虚拟地址不变,物理地址指向新的物理内存空间(页表改变),然后再做修改。
写时拷贝保证了进程的独立性,父子进程的运行不会互相影响。

对于两种不同目的的进程创建,都会发生写时拷贝,只不过一个是拷贝数据,一个是拷贝代码。

修改共享数据前
在这里插入图片描述
修改共享数据后
在这里插入图片描述


1.4 进程创建失败的场景

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

🎈进程创建了,运行结束后,进程的退出也是大有讲究,下面将要探讨进程退出。




2 进程退出

💭 目前我们知道,子进程在退出之后,父进程回收前,会保持僵尸状态,以保存其退出信息,并等待父进程回收。那么这里的退出信息是什么?父进程又是如何回收子进程的退出信息状态的?下面分析。

2.1 进程退出状态

📝一般来说,进程退出的场景有如下三种:

  1. 进程正常运行结束,运行结果正常
  2. 进程正常运行结束,但运行结果错误
  3. 进程异常终止。

🔎Linux中用 进程退出码(code) 表示进程正常结束的状态,用 进程退出信号(SIG) 表示进程异常退出的原因。二者的本质就是进程PCB中的两个数字,当进程处于僵尸状态时,程序退出,PCB保存着退出状态信息。


2.2 进程退出的方式

1️⃣正常退出

  1. main函数return退出
  2. 调用 exit 退出
  3. 调用_exit 退出

正常退出时,可以用echo指令在命令行上查看退出码。(正常退出时,退出信号为0)

echo $? // 查询最近结束的进程的退出码

2️⃣异常终止

  1. ctrl+c 终止前台进程
  2. 给进程发送终止信号

⭕图为kill指令提供的终止信号,一个数字代表一种信号。

在这里插入图片描述


2.3 exit、_exit、return

2.3.1 概念

  • exit

🔎 exit是一个用户级的函数,功能是终止当前执行的进程,并返回指定的退出码。

#include <stdlib.h> // 所在头文件
void exit(int status); // status是返回的退出码

一般规定:

  • exit(0) 正常退出
  • exit(!0) 异常退出

写一段C代码验证:

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

int main()
{
    printf("hello world\n");
    exit(123);
}

结果:

[ckf@VM-8-3-centos lesson7]$ ./test
hello world
[ckf@VM-8-3-centos lesson7]$ echo $?
123 // 查询到退出码为我们exit参数指定的值

💡 exit的参数statue默认只有低八位有效,且视为无符号,所以当我们给exit传入-1时,退出码为255。退出码的范围是[0~255]

验证

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

int main()
{
    printf("hello world\n");
    exit(-1);
}
[ckf@VM-8-3-centos lesson7]$ ./test
hello world
[ckf@VM-8-3-centos lesson7]$ echo $?
255
  • _exit

🔎与exit不同的是,_exit是一个系统调用接口,是由系统提供的(exit函数是C函数库提供的)。而exit最后也要调用_exit接口,相当于exit是对_exit的封装,功能比_exit更多。

#include <unistd.h> // 所在头文件
void _exit(int status); // 参数与exit函数相同
  • return

⭕ return是最为常规的进程退出方式。在main函数中执行return n相当于执行exit(n),因为调用main函数的运行时函数会将main函数的返回值当作exit的参数。需要注意的是,在main函数中return才能退出进程,在其它函数中return只起到函数返回的作用。

2.3.2 区别

  1. exit函数是C函数库提供的用户级函数,_exit是系统调用函数。
  2. return是C语言的关键字,exit、_exit是函数。
  3. return是语言级别的,表示调用堆栈的返回。而exit是系统调用级别的,表示一个进程结束
  4. return用于结束一个函数的执行,将函数的执行信息传出个其他调用函数使用;exit函数是退出应用程序,删除进程使用的内存空间,并将应用程序的一个状态返回给OS(操作系统),这个状态标识了应用程序的一些运行信息,这个信息和机器和操作系统有关,一般是 0 为正常退出,非0 为非正常退出。
  5. 非主函数中调用return和exit效果很明显,但是在main函数中调用return和exit的现象就很模糊,多数情况下现象都是一致的。

—— 参考文章《C语言中的exit()函数》




3 进程等待

3.1 理解进程等待

💭了解子进程如何退出并返回退出状态后,接下来就不得不提到,父进程该如何回收子进程的退出状态?

🔎进程退出后处于僵尸状态,程序退出,PCB暂时保留,其中保存着该进程的退出状态。父进程通过进程等待的方式获取子进程的退出状态,回收子进程资源。

为什么要进行进程等待?

防止内存泄漏!

一个进程一旦进入僵尸状态,即使用kill -9命令也无法将其杀掉,因为谁也没办法杀掉一个已经死去的进程。若父进程不回收处于僵尸状态的子进程,积少成多,很可能出现内存泄漏的问题

获知子进程的任务完成情况!
父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出


3.2 进程等待的方式

3.2.1 wait和waitpid

💭介绍两个用于进程等待的函数

  • wait
    功能:等待任一子进程退出,并回收子进程资源,获取子进程退出码
// 头文件
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int* status);

返回值:成功,返回被等待进程pid;
	   失败(如父进程没有子进程),返回-1;
	   
status:输出型参数,写入被等待进程的退出状态到该指针执行的空间中。不关心退出状态,可设置为NULL
  • waitpid
    功能:可通过pid指定某一子进程等待其退出,并回收子进程资源,获取子进程退出码
// 头文件
#include <sys/types.h>
#include <sys/wait.h>

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

返回值:被等待进程正常退出,返回被等待进程pid。
	   若options参数传入WNOHANG,调用中waitpid发现没有已退出的子进程可回收,则返回0。
	   调用失败(如pid不合法等),返回-1。
pid:
	pid>0:等待回收相应pid的子进程
	pid=-1:等待任一子进程,等价于wait

status
	写入被等待进程的退出状态
	
options:
	WNOHANG:若pid相应的子进程没有结束,waitpid返回0,不予以等待。若正常结束,返回子进程pid。

⭕注意:

  1. 如果子进程已经退出(此时的子进程是僵尸状态),调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  2. 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  3. 如果不存在(对应的)子进程,则立即出错返回。

💬 非法情况验证

情况1️⃣

int main()
{
    int ret = wait(NULL); // 当前进程并没有子进程,无法进程等待,wait调用失败
    
    printf("%d\n",ret);
    
    return 0;
}

⭕执行结果:

在这里插入图片描述

情况2️⃣

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

int main()
{
    pid_t id = fork();

    if(id == 0)
    {
        printf("I am child\n");
        exit(0);
    }

    int ret = waitpid(id+1,NULL,0); // pid为id+1的子进程不存在,无法进程等待,waitpid调用失败
    
    printf("%d\n",ret);
    
    return 0;
}

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


3.2.2 status位图结构

💭聊聊参数status
wait/waitpid都有一个参数status,该参数为一个输出型参数,是一个指针,指向一个int类型(32位)的变量,调用wait/waitpid时由OS向status指向的空间写入被等待进程的退出状态,若传入的status为NULL空指针,表示不关心被等待进程退出状态,OS则不会写入。不能将*status简单看成整型对待,其结构可视为位图结构(并且只研究低16位),具体如下图:

在这里插入图片描述
core dump标志:程序退出时,是否保存异常信息

  • 基于这样的位图结构,我们可以通过位运算的方法,提取退出码或退出信号。
int stat = *status

退出码:(stat>>8) & 0xff
退出信号:stat & 0x7f
core dump标志:(stat>>7) & 1

💬 来段代码测试一下:

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

int main()
{
    pid_t id = fork();
    
    if(id == 0)
    {
        printf("I am child procss, and my pid is %d\n",getpid());
        sleep(5);
        exit(6); // 传个6作为子进程退出码
    }

    int status = 0;
    waitpid(id,&status,0); // 等待五秒后,OS向status写入子进程退出状态

    printf("sig:%d\ncore dump flag:%d\nending code:%d\n",status & 0x7f,(status>>7) & 1,(status>>8) & 0xff);

    return 0;
}

⭕ 子进程正常退出的情况,退出码、退出信号等信息符合预期

在这里插入图片描述

⭕ 子进程被信号杀掉的情况,符合预期。

在这里插入图片描述

  • 除了位运算的方式,我们还可以利用宏函数来获取退出状态。

🔎wait/waitpid的头文件中还包含了一些宏函数,帮助我们通过status指向的值提取退出码、退出信号。这里简单介绍几个。

// 关于退出码
WIFEXITED(status): 若子进程正常终止,则为真(非零)。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

// 关于退出信号
WIFSIGNALED(status):若子进程异常终止(被信号所杀),则为真(非零)。(查看进程是否被信号所杀)
WTERMSIG(status):若WIFSIGNALED非零,提取子进程退出信号。(查看进程退出信号)

💬 来段代码测试一下:

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

int main()
{
    pid_t id = fork();
    
    if(id == 0)
    {
        printf("I am child procss, and my pid is %d\n",getpid());
        sleep(5);
        exit(6);
    }

    int status = 0;
    waitpid(id,&status,0);

    if(WIFEXITED(status))
    {
        int ending_code = WEXITSTATUS(status);
        printf("%d\n",ending_code);
    }

    if(WIFSIGNALED(status))
    {
        int sig = WTERMSIG(status);
        printf("%d\n",sig);
    }
    return 0;
}

两种情况都符合预期。

在这里插入图片描述


3.3 阻塞等待和非阻塞等待

  • 什么是阻塞等待?

💬 看如下代码图:

在这里插入图片描述

fork创建子进程后,子进程和父进程执行不同代码段,因为子进程要休眠5秒才退出,父进程waitpid无法直接回收子进程,所以父进程会阻塞在waitpid处,一直等待子进程退出并返回退出信息后,才会继续运行。这种情况的进程等待,称之为阻塞等待

  • 什么是非阻塞等待?

💭父进程阻塞等待子进程的过程无法进行其他操作,只能干等,降低了运行效率。若想让父进程在等待的过程还能执行其他任务,可以采用轮询的方法。

  1. 所谓轮询,就是让父进程间断性地去查询子进程是否退出,若子进程尚未退出,则不再阻塞等待,执行其他任务,若子进程已退出,则进行相关的回收操作。
  2. 父进程以轮询的方式等待子进程退出,这种方法我们称之为非阻塞等待

实现非阻塞等待的方法:

给waitpid的第三个参数options传入宏WNOHANG。传入后,若waitpid发现相应pid的子进程尚未退出,直接返回0。利用这一特点结合while循环,便可实现非阻塞等待

💬具体代码实现

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

void other_task()
{
    printf("The child process is still running, parent process is running other_task\n");
}

int main()
{
    pid_t id = fork();
    
    if(id == 0)
    {
       // printf("I am child procss, and my pid is %d\n",getpid());
        printf("I am child process, and I am running\n");
        sleep(5);
        exit(0);
    }
    
    int status = 0;
    while(1)
    {
        pid_t ret = waitpid(id,&status,WNOHANG);
        assert(ret>=0);

        if(ret > 0) // 子进程已退出
        {
            printf("ending code:%d\n",(status>>8) & 0xff);
            break; // 退出循环,结束轮询
        }

        else if(ret == 0) // 子进程尚未退出
        {
            other_task(); // 父进程执行其他任务
            sleep(1); // 休眠一秒后再进行下一次轮询
        }
    }
    return 0;
}

⭕测试结果

在这里插入图片描述




4 进程替换

💭 还记得进程创建的目的吗?

  1. 父进程希望生成一份自己的副本,执行同一个程序中不同的代码片段。
  2. 让子进程执行不同的程序。

上文讨论的都是围绕进程创建第一个目的,而下面我们要谈谈如何让子进程执行不同的程序。

4.1 进程替换的原理

⭕ 想让子进程执行不同的程序,必须依赖于进程替换。使用fork函数创建子进程后,子进程与父进程的程序是相同的(只是可能执行不同分支),若想让子进程执行另一个程序,要用exec系列函数对子进程进行进程替换,执行新的程序

值得注意的是,进程替换所替换的是进程的代码和数据,没有创建新的PCB,所以进程还是那个进程,pid不变。

另外,进程替换时会发生写时拷贝!保证父进程与子进程的独立性。从磁盘上加载新的代码程序到内存中时,子进程的页表会发生改变。

在这里插入图片描述


4.2 进程替换的函数

4.2.1 认识exec函数

💭 实现进程替换的函数是命名以exec开头的一系列函数,总共有六个

#include <unistd.h> // 所在头文件

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[]);

参数:

  1. path: 欲替换程序的路径
  2. file: 欲替换程序的名称
  3. arg: 命令行参数
  4. argv[]: 命令行参数数组
  5. envp[]: 环境变量表

注意:参数必须以NULL结尾

💭很好理解,要进行进程替换就要解决两个问题。去哪找程序来替换?替换后如何执行?上面这六个函数的参数用以解决这两个问题,参数的不同代表解决方法不同,path和file解决了去哪找的问题,而arg、argv[]解决了如何执行程序的问题。最后,我们还可以通过envp[]设置子进程的环境变量。

🔎这些函数若调用成功则加载新的程序并执行,不会返回值。只有调用失败时,才会返回-1。所以exec系列函数只有出错的返回值而没有成功的返回值。

🌰举个栗子,父进程创建一个执行ls程序的子进程

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

int main()
{
    int id = fork();

    if(id == 0) // 子进程
    {
        execl("/usr/bin/ls","ls","-l","-a","--color=auto",NULL);
        exit(1); // 因为execl会替换新的程序,所以如果子进程走到这里替换必定失败
    }

    wait(NULL); // 父进程回收子进程
    return 0;
}

对比命令行下执行ls,发现符合预期。

在这里插入图片描述

💭其实还有一个函数execve,属于系统调用接口。以上6个函数都是对它进行封装得到的,底层都是调用execve。

#include <unistd.h>
int execve(const char *filename, char* const argv[], char* const envp[]);

4.2.2 exec函数的命名理解

💭 掌握exec函数的命名规则,理解其意义,才能灵活地调用。

函数名带意义
l (list)以列表的形式传递参数
v(vector)以数组的形式传递参数
p(path)直接传程序名,OS会从环境变量PATH中的路径去找
e(environ)可自定义环境变量

5. 制作一个简易的Shell

💡综合进程创建、进程退出、进程等待和进程替换的知识,我们可以模拟制作一个简易的shell

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAX_NUM 1024
#define ARG_NUM 64

char buf[MAX_NUM] = {0};

int main()
{
    while(1)
    {
        // 用户标识符
        char* p = getenv("PWD");
        p+=strlen(p);
        while(*(p-1)!='/')
        {
            --p;
        }
            
        printf("[%s@%s %s]$ ",getenv("USER"),getenv("HOSTNAME"),p);
        fflush(stdout);

        // 获取参数
        char* s = fgets(buf,sizeof(buf)-1,stdin);
        s[strlen(s)-1] = '\0';
        assert(s!=NULL);
        
        // 分割字符串
        char* my_argv[ARG_NUM];
        my_argv[0] = strtok(buf," ");
        int i = 1;
        while(my_argv[i++] = strtok(NULL," "));

        // 进程替换
        pid_t id = fork();
        if(id == 0)
        {
            execvp(my_argv[0],my_argv);
            exit(1);
        }

        wait(NULL);
    }

    return 0;
}

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

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

相关文章

【一起从0开始学习人工智能0x04】python相关AI复习【超全面】【收藏】

基础小结 数据类型&#xff1a;Numbers、String、list、tuple、dict字符编码&#xff1a;可以直接.encode&#xff08;‘utf-8’&#xff09;、.decode(ascii)循环&#xff1a;for-in迭代器、whilelist列表-最频繁的数据类型。-完成大多数集合类的数据结构实现。它支持字符&am…

十六、状态管理——Vuex(4)

本章概要 表单处理Vuex 与组合 API模块 16.8 表单处理 在表单控件上通常会使用 v-model 指令进行数据绑定&#xff0c;如果绑定的数据是 Vuex 中的状态数据&#xff0c;就会遇到一些问题。看以下代码&#xff1a; form.html <div id"app"><my-component&…

一场衍生于产业的深度变革正在上演,总结起来就是产业互联网的变革

毫无疑问的是&#xff0c;一场衍生于产业的深度变革正在上演。在这样一场深度变革之中&#xff0c;曾经看似无法改变的存在&#xff0c;有了进化的可能性&#xff1b;曾经让玩家们望而却步的领域&#xff0c;有了进军的可能性。如果对这样一场深度变革进行一次总结的话&#xf…

vanishing point detection in autopilot

1. 概述 消失点一种直观的解释是图像中的平行线的交点&#xff0c;也就如下图中路面边界绘制的直线在图像中的交点。 这样的点在自动驾驶场景下可以为解析车辆状态提供一些信息&#xff0c;比如较为常规的运用便是用于车辆的pitch角度。在传统方法中会通过如霍夫算子检测图片…

2022稳定学习年度研究进展系列报告丨精华观点总结

近年来&#xff0c;在独立分布假设的前提下&#xff0c;机器学习模型的表现越来越好。但在实际应用场景中&#xff0c;数据本身却具有很强的异质性和差异性&#xff0c;这就对模型的泛化能力产生了较高的要求。为了解决分布外泛化问题&#xff0c;稳定学习应运而生。12月28日&a…

Python计算机视觉:人脸识别

讲明一下:并没有实现人脸识别的算法,只是利用人脸特征文件(文件从官网上下载),从而进行人脸识别,总感觉识别出来的效果还是有问题的,如:图片最好是人脸的正脸。 1. 人脸特征文件下载 直接去github或者gitee(建议gitee)上去搜索opencv即可,如下: 选择第一个直接进入即…

一篇文章带你了解——Linux中 文件权限 和 粘滞位的 概念 / 作用 及 实现方法

粘滞位首言用户权限文件权限文件类型分类文件访问者的分类实际解读文件权限文件权限设置方法为啥要有文件权限为啥要有粘滞位粘滞位的好处如何添加粘滞位首言 要了解粘滞位&#xff0c;首先得了解文件及用户权限 用户权限 Linux下有两种用户&#xff1a;超级用户&#xff08…

共享内存原理与使用

共享内存是System V版本的最后一个进程间通信方式。共享内存&#xff0c;顾名思义就是允许两个不相关的进程访问同一个逻辑内存&#xff0c;共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一…

vite学习详解

简介 前言 之前vite2刚出的时候其实已经自学过一波&#xff0c;但是老实说学起来完全不入脑&#xff0c;一方面本来这方面的基础就很差&#xff08;指项目配置&#xff09;&#xff0c;另一方面学的时候没有跟着去动手&#xff0c;纯理论的学那完全就是越看越困。最后就是急躁…

java书店带商家商城书店多单商书店系统源码

简介 Java ssm开发的多商家书店商城&#xff0c;用户可以浏览商品&#xff0c;加入购物车&#xff0c;直接下单支付&#xff0c;在我的个人中心里可以管理自己的订单&#xff0c;收货地址&#xff0c;编辑资料等&#xff0c;还可以申请开店&#xff0c;店铺开通后可以发布商品…

C++--list

前言 这篇文章对于理解封装是非常有帮助的&#xff0c;list的底层是双向链表结构&#xff0c;我们在学习数据结构是就已经学过了双向链表&#xff0c;双向链表中每个元素存储在互不相关的独立节点中&#xff0c;在节点中通过指针指向其前一个元素和后一个元素。因为list独特的结…

nsis打包程序自动生成的快捷方式打不开

nsis 打包程序自动生成的快捷方式打不开 一: 问题描述: nsis 打包程序自动生成的快捷方式打不开, 报的是: 打不开数据库 , 但是在目录下双击exe 是能够打开的 一: 问题推导: 我是先右击自动生成的快捷方式 选择打开文件所在位置, 我发现确实是 我想要那个exe 的所在位置然后…

Zynq PL端调用PS端的时钟

ZYNQ PS端最多可以分配4个时钟供给PL端使用&#xff0c;见下图。 本文的目的&#xff1a;在XCZU21DR环境下&#xff0c;PS给PL提供一个100MHz的时钟&#xff0c;PL端根据此时钟产生1S信号&#xff0c;点亮LED。 添加&配置Zynq UltraScale MPSoc IP 双击该IP&#xff0c;在…

【C++】C++入门知识(一)

作者&#xff1a;一个喜欢猫咪的的程序员 专栏&#xff1a;《C》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 目录 1.C关键字 2.命名空间 2.1局部变量和全局变量 2.2命名空间的概念及使用 2.3…

本硕985计算机,34岁的字节大头兵,上司很器重我,给我加薪不少,但国企也欢迎我,好犹豫该不该去国企!...

互联网VS国企&#xff0c;该怎么选&#xff1f;这是一位34岁的字节程序员面临的选择&#xff1a;在头条是2-2大头兵&#xff0c;本硕985计算机&#xff0c;国企还是比较欢迎他的。原本的想法是在私企干几年&#xff0c;如果干不成管理就去国企。如今没当成管理&#xff0c;但上…

【蓝桥杯嵌入式】第十三届蓝桥杯嵌入式省赛(第二场)程序设计试题及其题解

原题展示 &#x1f4c4; 本试题目的是制作一个商品管理系统&#xff0c;其主要功能为&#xff1a;购买商品、增加商品储量、调节商品价格、查询商品价格&#xff0c;并且能够保存改变后的商品数量与商品价格&#xff0c;总体上看跟第一场的试题差不多&#xff0c;下面就让我们一…

Python Selenium 获取动态网页指定元素的超链接

Python Selenium 获取动态网页指定元素的超链接前言前提条件相关介绍实验环境获取动态网页指定元素的超链接目标网址代码实现前言 本文是个人使用Python Selenium 获取动态网页指定元素的超链接的电子笔记&#xff0c;由于水平有限&#xff0c;难免出现错漏&#xff0c;敬请批评…

详解浮点数在内存中的存储

目录 前言 一、 32 位单精度浮点数在内存中的存储 1.1 - 符号位 sign 1.2 - 偏移后的指数位 biased exponent 1.3 - 尾数位 fraction&#xff08;mantissa&#xff09; 二、64 位双精度浮点数在内存中的存储 三、浮点数的比较 前言 计算机内部实际上只能存储和识别二进制…

IPV6相关

目录 一、IPV6地址组成与专业术语 1.基础分类 2.本地链路地址范围区域概念 3.本地环回地址 二、centos配置IPV6地址 1.终端命令配置IPV6地址和网关 2.文件中配置IPV6地址 三、IPV6连通测试 1.全局单播地址进行ping 一、IPV6地址组成与专业术语 1.基础分类 IPv6基础知…

对话开发者:Serverless 落地的困境与破局

作者 | 阿里云开发者社区、InfoQ 从 2012 年提出 Serverless 到今年 2022 年刚好十年。 过去十年&#xff0c;上云是确定性趋势&#xff0c;在这个阶段企业一开始的关注点在于如何实现平滑上云。随着越来越多的企业上云&#xff0c;甚至很多企业系统第一天就是在云上构建&…