【Linux】解锁系统编程奥秘,高效进程控制的实战技巧

2024-09-06 14:20

本文主要是介绍【Linux】解锁系统编程奥秘,高效进程控制的实战技巧,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

进程控制

  • 1. 进程创建
    • 1.1. 操作系统的工作内容
    • 1.2. fork常规用法
    • 1.3. fork调用失败的原因
  • 2. 进程终止
    • 2.1. main函数的返回值
      • 2.1.1. 退出码
      • 2.1.2. 退出码转化为错误描述的方式
    • 2.2. 普通函数的返回值
      • 2.2.1. 错误码
    • 2.3. 进程退出的场景
    • 2.4. 进程退出的方式
      • 2.4.1. main函数的返回
      • 2.4.2. 调用exit()、_exit()函数
      • 2.4.3. exit()、_exit()函数的区别
  • 3. 进程等待
    • 3.1. 必要性
    • 3.2. 方式
      • 3.2.1. wait
      • 3.2.2. waitpid
    • 3.3. 阻塞等待、非阻塞状态</h2>
    • 3.4. 获取子进程
      • 3.4.1. 位操作
      • 3.4.2. 宏
  • 4. 进程程序替换
    • 4.1. 概念与原理
    • 4.2. 替换函数exec*
      • 4.2.1. 细节
    • 4.3. 多进程版本替换</h2>
    • 4.4. 应用场景</h2>

1. 进程创建

1.1. 操作系统的工作内容

当一个进程调用fork函数创建一个新的子进程时,操作系统会执行以下主要步骤:分配资源 -> 复制信息 ->添加到系统进程列表 -> 返回值 -> 调度器调度。

  • 分配资源:OS给子进程分配新的内存块(虚拟地址空间),和其他必要的内核数据结构(PCB、页表等)。

  • 复制信息:将父进程的数据结构内容的一部分拷贝给子进程。

  • 添加到系统进程列表:OS会将子进程的信息添加到系统的进程管理数据结构中(进程链表等),这样系统就能够管理和跟踪该子进程的状态和行为,调度器就能够感知新的进程并对其进行调度。

  • 返回值:为了区分父子进程,并允许它们执行不同的代码路径(if…else分流)。

  • 调度器调度:一旦fork操作完成,OS调度器就会重新安排CPU时间片,调度器可能会根据调度算法选择父进程、子进程、或系统中其他进程来执行,这意味着父进程,子进程可能会并发执行,但它们的执行顺序是不确定的。

1.2. fork常规用法

  1. 父进程希望复制自己,根据fork返回值使用if…else进行分流,从而使父子进程执行相同程序中不同的代码段。例如:父进程等待客户端请求,生成子进程来处理请求。

  2. 子进程要执行与父进程完全不同的程序。在这种情况下,子进程从fork返回后,通过调用exec()系列函数,在当前进程中加载并运行一个新的程序(进程程序替换)。例如:需要执行特定任务的子进程,这些任务与父进程的主要职责不同。

1.3. fork调用失败的原因

  1. 系统中有太多进程。如:一个学校能容纳的学生总数是有限的。

  2. 实际用户的进程数超过了限制。如:一个班级能容纳的学生总数是有限的。

2. 进程终止

2.1. main函数的返回值

2.1.1. 退出码

  1. 退出码:main函数的返回值,用来表示进程退出(终止)时,其执行结果是否正确。

  2. main函数返回0,表示代码执行成功,结果正确或者符合预期的。

  3. main函数返回非0,表示代码执行成功,结果是不正确的或程序遇到错误/异常情况。

代码执行成功,程序能够执行到main函数的末尾并返回,而不是说程序中的每一行都按预期执行了,因为有些错误不能被捕获或者导致程序提前退出了。

  1. 非0返回值,通常用于表示不同类型错误/异常的原因,退出码的字符串含义取决于程序的设计者。

  2. Shell(bash)会自动记录最近一次子进程执行完毕时的退出码,这个退出码可以通过echo $?获取。

echo $?

  • 功能:获取上一个命令的退出码。
#include<stdio.h>int main()
{printf("hello world\n");return 0;  //退出码                                                              }       

2.1.2. 退出码转化为错误描述的方式

  1. 使用语言或者系统自带的方法进行转化,例如:在linux中,使用strerror()函数。

char* strerror(int errnum);

#include<stdio.h>
#include<string.h>                                                                                                                             
int main()
{for(int i = 0; i < 100; i++)printf("%d:%s\n", i, strerror(i));return 0;
}


2. 使用枚举类型进行自定义。

#include<stdio.h>
#include<string.h>enum{success=0,malloc_err,open_err
};const char* errorDesc(int code)
{switch(code){case success:return "running sucess!";case malloc_err:return "malloc failure!";case open_err:return "file open failure!";default:return "unkown error!";                                                }
}int main()
{int exit_code = malloc_err;printf("%s\n", errorDesc(exit_code));return 0;
}

2.2. 普通函数的返回值

执行结果:文件打开成功,fopen()返回指向该文件的指针;文件打开失败,fopen()返回NULL。

执行情况:返回了非空的FILE*指针,则可认为函数执行成功;返回了NULL,则可认为函数执行失败,需要进一步检查错误的原因(errno变量或调用perror()函数)。

  1. 普通函数退出,仅仅表示函数调用完毕。

  2. 函数也被称为子程序,与进程退出时返回退出码类似,函数执行完毕也会返回一个值,这个值通常用于表示函数的执行结果或状态。

  3. 调用函数,我们通常想看到两种结果:a.函数的执行结果(函数的返回值);b.函数的执行情况(函数是否成功执行了预期的任务),例如:fopen()函数的执行情况是通过其执行结果来间接表示。

2.2.1. 错误码

  1. errno是错误码,它是记录系统最后一次错误代码的一个整数值,不同值表示不同含义,在#include<errno.h>中定义。

  2. 当函数运行成功时,errno值不会被修改,因此我们不能通过测试errno的值来判断是否有错误存在,而应该在被调用的函数提示有错误发生时,再检查errno的值。

💡Tips:只有当库函数失败时,errno的值才会被设置。

#include<stdio.h>
#include<string.h>
#include<errno.h> int main()
{FILE* fp = fopen("log.txt","r");if(fp == NULL)  printf("%d:%s\n", errno, strerror(errno)); return 0;
}

2.3. 进程退出的场景

退出码用于表示程序的执行结果或者终止的原因。

退出信号(kill)用于指示进程是正常退出,还是被信号杀死(每个信号都有不同的编号,编号表明异常的原因)。

  1. 代码执行完毕,结果正确 -> 退出信号=0、退出码=0。

  2. 代码执行完毕,结果不正确 -> 退出信号=0、退出码=!0。

  3. 代码没有执行完毕,进程异常终止 -> 退出信号=!0、退出码无意义。

💡Tips:任何进程最终的执行情况,我们都可以用两个数字来表明具体的执行情况,这两个数字分别为退出码、退出信号。

2.4. 进程退出的方式

2.4.1. main函数的返回

  1. 当程序执行到main函数的末尾,或者遇到return语句时,程序会返回main函数的返回值(退出码)。

  2. mian函数返回是程序主动退出的方式,即:正常终止进程。

2.4.2. 调用exit()、_exit()函数

一、exit()函数

void exit(int status);

  1. 调用exit()是程序主动退出的方式,即:正常终止进程。

  2. exit()函数是C标准库提供的一个函数,在#include<stdlib.h>中定义,用于立即终止当前进程的执行,它会接受一个整形作为参数,该整形为进程的退出码。

  3. 调用exit(),程序会立即终止,exit()同时会执行清理操作(如:刷新所有的输出缓冲区、关闭通过fopen打开的文件、malloc开辟的内存等),然后向操作系统返回退出码。

二、_exit()函数

void _exit(int status);

  1. 调用_exit()是程序主动退出的方式,即:正常终止进程。

  2. _exit()函数是系统调用函数,在#include<unistd.h>中定义,用于立即终止当前进程的执行,它会接受一个整形作为参数,该整形为进程的退出码。

  3. _exit()不会自动执行exit()函数所执行的清理工作,需要确保在调用它之前,手动处理所有必要的清理工作,然后向操作系统返回退出码。

💡注意:main函数返回、调用exit()、_exit()函数,都表示程序主动退出,即:正常终止;接受到信号(如:ctrl c,信号终止),表示程序被动退出,即:异常退出。

2.4.3. exit()、_exit()函数的区别

  1. exit()支持刷新缓冲区,_exit()不支持刷新缓冲区,因为exit中有缓存区,_exit中无缓冲区。

  2. 它们都是终止进程,但只有OS才有能力终止进程,因此exit()底层封装了_exit(),两者是上下层关系,。

💡Tips:我们之前谈及的缓冲区(进度条之类),绝对不是操作系统级别的缓冲区,是标准C库管理的缓冲区(库级别的缓冲区)。

为什么语言具有可移植性和跨平台性?在库层面上,对系统强相关的接口进行了封装,从而屏蔽了底层差异。

3. 进程等待

3.1. 必要性

  1. 子进程先退出,如果父进程不回收其资源,子进程就变成僵尸状态,此状态无法被kill -9杀死,因为无法杀死已经死掉的进程,从而造成内存泄漏。

  2. 父进程需要知道派给子进程的任务完成的如何,即:获取子进程的退出信息。

子进程的退出信息(exit code、exit signal),需要通过内核数据结构来维护,保存在子进程的task_struct中,属于内核数据。

💡Tips:总结:父进程通过进程等待的方式,回收子进程资源(必然),获取子进程的退出信息(可选)。

3.2. 方式

3.2.1. wait

pid_t wait(int* status);

  • 功能:等待任意一个子进程结束,并回收其资源。
  1. 返回值:调用成功,返回已经结束进程的PID,同时获取到了子进程的退出状态码;调用失败,返回-1,并设置错误码以指示错误的原因。

  2. 参数status:输出型参数,用于存储子进程的退出状态,由OS填充,如果不需要这个信息,可以传递NULL,否则,OS会根据该参数,将子进程的信息反馈给父进程。

3.2.2. waitpid

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

  • 功能:等待任意一个子进程或者指定的子进程结束,并回收其资源。
  1. 参数pid:如果pid = -1,等待任意一个子进程,与wait等效;如果pid > 0,等待其进程的PID与pid相等的子进程。

  2. 参数option:如果option = 0,则为阻塞等待;如果option = WNOHANG,则为非阻塞等待。

  3. 返回值:调用成功,返回收集到的子进程的PID,同时获取到了子进程的退出状态码;调用失败,返回-1,并设置错误码以指示错误的原因;如果为非阻塞等待,waitpid调用成功且没有收集到已结束的子进程,则返回0。

3.3. 阻塞等待、非阻塞状态

一、阻塞等待

  1. 定义:进程在发出某个请求(如:I/O操作、等待某个条件成立等)后,如果请求不能立即得到满足(如:数据未准备好、资源被占用等),进程会被挂起,在此期间无法继续执行其他任务,直到等待条件满足或被唤醒。

故事理解:张三找李四辅导c语言,张三打电话给李四叫他下来,但李四正在寝室中复习期末,李四在复习的期间,和张三一直通着电话,张三不能做任何其他事情。打电话的过程 == 系统调用。

  1. 特点:

a.行为 -> 进程在等待期间无法执行其他任务。

b.触发方式 -> 等待由外部条件触发(如:数据到达、资源释放等)。

c.管理层面:由操作系统或者底层系统资源管理。

d.效率与并发性:效率低。

  1. 应用场景:实时性要求不高,等待时间相对比较短的情况,如:简单文件的读写操作。
#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) //子进程{int cnt = 5;while(cnt){printf("child is running, id:%d, ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}exit(1); //子进程退出}int status = 0; //存储子进程退出状态pid_t rid = waitpid(id, &status, 0); //父进程等待 —— 阻塞等待if(rid > 0) //等待成功printf("wait success, status:%d\n", status);else if(rid == -1) //调用失败perror("wait error!\n");                                     return 0;
}


二、非阻塞等待

  1. 定义:进程在发出某个请求后,不会被立即挂起已等待请求的完成,即使请求不能立即得到满足,进程在等待期间可以继续执行其他任务,同时可能会以某种方式(轮询访问、回调等)定期检查请求状态或者等待结果的通知。

故事理解:张三找李四辅导c语言,张三打电话给李四叫他下来,但李四正在寝室中复习期末,电话挂断,李四在复习的期间,张三可以做其他的事情,并定期多次打电话给李四,询问是否复习完毕。

  1. 特点:

a.行为 -> 进程在等待期间可以执行其他任务;

b.触发方式 -> 可能通过编程的方式实现,如:轮询、回调等。

c.管理层面:在应用层通过编程实现。

d.效率与并发性:效率高,提高并发性和响应能力。

  1. 应用场景:需要高并发和响应能力的场景,如:在网络编程中,服务器同时处理多个客户端的请求。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>#define SIZE 5 typedef void(*fun_t)(); //函数指针类型fun_t task[SIZE]; //函数指针数组void printlog()
{printf("this is a log print task\n");
}void printnet()
{printf("this is a net task\n");
}void printNPC()
{printf("this is a flush NPC task\n");
}                                                                                                                            
void Inittask()
{task[0] = printlog;task[1] = printnet; task[2] = printNPC;task[3] = NULL;
}void executeTask()
{for(int i = 0; task[i]; i++)task[i]();  //回调函数机制                                                        }int main()
{Inittask();pid_t id = fork(); if(id == 0) //子进程{int cnt = 2;while(cnt){printf("I am a process, id:%d, ppid:%d\n", getpid(), getppid());             			sleep(1);cnt--;}exit(1); //子进程退出}int status = 0; //存储子进程退出状态while(1) //基于非阻塞轮询的访问{pid_t rid = waitpid(id, &status, WNOHANG); //非阻塞等待if(rid > 0) //调用成功,收集到了已经结束的子进程                                            {printf("wait success, status:%d\n", status);break;}else if(rid == 0) //调用成功,未收集到已经结束的子进程{printf("child is running, father do other thing!\n");printf("------------ Task begin ----------------\n");executeTask(); //等待期间,执行其他任务 printf("------------ Task end ----------------\n");}else //调用失败{perror("wait error\n");break;}sleep(1);}return 0;
}

3.4. 获取子进程

status不能简单的当作整形来看,可以当作位图看待,它有自己的格式,只研究status低16位比特位。

3.4.1. 位操作

#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) //子进程{int cnt = 5;while(cnt){printf("child is running, id:%d, ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}exit(1); //子进程退出}int status = 0; //存储子进程退出状态pid_t rid = waitpid(id, &status, 0); if(rid > 0) //等待成功printf("wait success, status:%d, exit code:%d, exit sign:%d\n", status, (status>>8)&0xff, status&0x7f);   //位操作获取子进程的退出码、退出信号        return 0;
}

3.4.2. 宏

  1. WIFEXITED(status):检查子进程是否正常退出。

如果子进程通过调用exit函数或main函数return返回而退出,则WIFEXITED返回非0值(真)-》正常退出;如果子进程是由于接收到信号而退出,则WIFEXITED返回0(假)-》异常退出。

  1. WEXITSTATUS(status):只有当WIFEXITED为真时,接着才会使用WEXITSTATUS获取子进程的退出码。
#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) //子进程{int cnt = 5;while(cnt){printf("child is running, id:%d, ppid:%d\n", getpid(), getppid());sleep(1);cnt--;}exit(1); //子进程退出}int status = 0; //存储子进程退出状态pid_t rid = waitpid(id, &status, 0); if(rid > 0) //等待成功{if(WIFEXITED(status)) //子进程正常退出printf("wait success, status:%d, exit code:%d\n", status, WEXITSTATUS(status)); //提取退出码 宏else  //子进程异常退出                                                      printf("child process error!\n");}return 0;
}

问题1:在父进程中定义两个全局变量(exit code、exit sign),子进程修改exit code值,父进程可以获取到子进程的退出信息吗?

  • 不能。因为进程具有独立性,子进程对共享数据的修改,父进程是不可见的。

问题2:为什么要有wait、waitpid?

  • 为了避免子进程僵尸,造成内存泄漏,父进程需要通过wait、waitpid等函数来回收子进程资源,同时可以获取到子进程的退出信息。

  • 子进程的退出码、退出信号等内核数据,需要被拷贝到用户层的某个变量(如:wait、waitpid中的status参数等),这个过程需要调用系统调用接口,因为用户空间的程序无法直接访问内核空间的数据。

4. 进程程序替换

4.1. 概念与原理

  1. 概念:它允许一个进程在执行期间,用一个新的程序来替换当前正常执行的程序,即:用全新的程序替换原有的程序。

这意味着进程在调用一种exec函数,当前进程的用户空间代码和数据被新程序的代码和数据完全替换(覆盖),从新程序的启动例程开始执行。

💡Tips:调用exec函数,并不会创建新的进程,而是对原有进程的资源进行替换,因此调用exec前后该进程的pid并未发生改变。

  1. 原理:加载新程序 -> 替换当前程序 -> 更新页表 -> 执行新程序。
  • 加载新程序:当进程决定进行程序替换时(调用exec函数),它会请求OS将全新程序(代码和数据)从磁盘中加载到内存。

  • 更新页表:为了实现替换,OS需要更新页表,将原来指向旧程序代码的虚拟地址映射到新程序代码的物理地址上,这样,就会执行新程序的代码。

  1. 所谓的把磁盘的数据加载到内存,把磁盘的数据拷贝到内存中,磁盘,内存都是硬件,只有操作系统具有将数据从一个硬件(磁盘)搬移到另一个硬件(内存)的能力,从而支持程序的加载和替换。

💡注意:进程替换的本质工作就是加载,充当的是加载器的角色!

4.2. 替换函数exec*

  1. l(list):有l表示命令行参数采用列表;v(vector):有v表示命令行参数采用数组;

  2. p(path):有p自动去环境变量中搜索,允许只写程序名);

  3. e(env):有e表示设置全新的环境变量,需要自己维护。


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

解释:以列表的形式传递命令行参数,最后必须以NULL结尾,第一个参数必须是程序的绝对路径。

#include<stdio.h>
#include<unistd.h> int main()
{printf("I am a process\n");//以列表的形式传参(l); 命令行怎么写,参数怎么传(可变参数列表),但结尾必须以NULL结尾execl("/usr/bin/ls", "ls", "-l", NULL);                                                                                  return 0;
}

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

解释:以数组的形式传递命令行参数,将命令行参数存储在以NULL指针结尾的指针数组中。

char* const argv[] = {(char*)"ls", (char*)"-l", NULL}; //存储命令行参数的以NULL结尾的指针数组
//以数组的形式传参(v)                                                                      
execv("/usr/bin/ls", argv);

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

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

解释:第一个参数允许用户只提提供程序名,在替换时,自动去环境变量PATH指定的路径中查找程序。

char* const argv[] = {(char*)"ls", (char*)"-l", NULL}; //存储命令行参数的以NULL结尾的指针数组
//有p自动去环境PATH中搜索,允许第一个参数只写程序名                                   
execlp("bin", "ls", "-l", "NULL");
execvp("bin", argv);     

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

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

int execve(const char* filename,char* const argv[ ],char* const envp[ ]);

解释:最后一个参数为自己维护的环境变量(可设置全新的环境变量表)。

#include<stdio.h>
#include<unistd.h> int main()
{printf("I am a process\n");char* const env[] = {(char*)"haha=hehe", (char*)"zzx=lala", NULL}; //自己维护环境变量execle("./mytest", "-a", "-b", NULL, env); //设置全新的环境变量return 0;
}
#include<stdio.h>int main(int argc, char* argv[], char* env[])
{for(int i = 0; argv[i]; i++)printf("argv[%d]:%s\n", i, argv[i]);for(int i = 0; env[i]; i++)printf("env[%d]:%s\n", i, env[i]);return 0;                                                                           
}

4.2.1. 细节

  1. 替换完成,不会创建新的进程。

  2. 程序一旦替换成功,exec*后续的代码不在执行。

  3. exec*函数只有出错的返回值,没有成功的返回值。

exec*函数调用失败,返回-1,并设置错误码以指示错误的原因。

  1. 进程替换本身不会改变环境变量的数据。

子进程会自动继承父进程的环境变量,子进程通过exec函数进行程序替换,加载并执行新的程序,这个新的程序会继承子进程所拥有的环境变量。

#include<stdio.h> 
int main()
{extern char** environ;for(int i = 0; environ[i]; i++)printf("%s\n", environ[i]);return 0;                                                                    
}
#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){execl("./mytest", "mytest", NULL);exit(0);}pid_t rid = waitpid(id, NULL, 0);if(rid > 0)printf("father wait success!\n");                                              return 0;
}
  1. int putenv(char* string);
  • 功能:动态改变或新增环境变量。

如果指定的环境变量已经存在,那么它的值会被新的字符串中的值所替换;如果指定的环境变量不存在,那么它会被添加到环境变量表中。

4.3. 多进程版本替换

多进程版本替换通常设计到父进程创建子进程,然后子进程使用exec函数来替换其执行的程序。

#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 a child process\n");execl("/usr/bin/ls", "ls", "-l", NULL);printf("程序替换成功\n");exit(0);}pid_t rid = waitpid(id, NULL, 0);if(rid > 0)printf("father wait success!\n");                                              return 0;
}

问题1:为什么子进程进行替换,父进程无任何影响?

  • 进程具有独立性:每个进程都有自己的地址空间,意味着每个进程只能访问自己的内存区域,而不能访问其他进程的内存区域,所以子进程进行程序替换,只会改变自己的地址空间的内容,不会影响到父进程的地址空间。

  • exec函数的行为:仅在调用它的进程中生效,而不会影响到父进程。由于exec函数是在子进程中调用的,因此只有子进程的映像被替换,父进程的映像保持不变,父进程继续执行其后续代码。

问题2:shell是如何执行起来一个指令的?

  • 读取命令行输入 -> 解析命令 -> 创建子进程 -> 执行程序替换 -> 等待子进程结束。

问题3:exec函数可以执行系统的指令(程序),它可以执行我们自己编写的程序吗?

  • 可以。无论你使用什么编程语言(c++、python等)编写程序,只要该程序可以被编译或者解释成可执行格式,并且位于系统可以访问的路径中,你就可以通过shell使用exec函数来执行它。

💡Tips:.cc、.cpp、.cxx都是c++的后缀。

4.4. 应用场景

进程替换的应用场景有:Shell命令解释、服务器设计、在线OJ、搜索引擎等

  1. Shell命令解释:当用户在Shell中输入一个命令,Shell会创建一个子进程来执行该命令,这个子进程会使用exec函数来替换需执行命令的代码和数据,从而执行用户指定的程序。

  2. 服务器设计:在服务器程序中,父进程可以创建多个子程序来处理客户端的请求,每个子程序可以使用exec函数来执行特定的程序或者服务。

补充:在现代计算机系统中,内核数据结构主要使用的是物理地址,而虚拟地址是OS给用户提供的功能模块。虚拟地址到物理地址的转换是由操作系统和硬件协同完成的,而且这个转化规则相对简单。

这篇关于【Linux】解锁系统编程奥秘,高效进程控制的实战技巧的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1142246

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

基于人工智能的图像分类系统

目录 引言项目背景环境准备 硬件要求软件安装与配置系统设计 系统架构关键技术代码示例 数据预处理模型训练模型预测应用场景结论 1. 引言 图像分类是计算机视觉中的一个重要任务,目标是自动识别图像中的对象类别。通过卷积神经网络(CNN)等深度学习技术,我们可以构建高效的图像分类系统,广泛应用于自动驾驶、医疗影像诊断、监控分析等领域。本文将介绍如何构建一个基于人工智能的图像分类系统,包括环境

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

高效+灵活,万博智云全球发布AWS无代理跨云容灾方案!

摘要 近日,万博智云推出了基于AWS的无代理跨云容灾解决方案,并与拉丁美洲,中东,亚洲的合作伙伴面向全球开展了联合发布。这一方案以AWS应用环境为基础,将HyperBDR平台的高效、灵活和成本效益优势与无代理功能相结合,为全球企业带来实现了更便捷、经济的数据保护。 一、全球联合发布 9月2日,万博智云CEO Michael Wong在线上平台发布AWS无代理跨云容灾解决方案的阐述视频,介绍了