Linux学习之进程三

目录

进程控制

fork函数

什么是写时拷贝

进程终止

mian函数的返回值

退出码

错误码

 exit()

进程等待

1.什么是进程等待?

2.为什么要进行进程等待?

3.如何进程进程等待?

wait,waitpid:

waitpid

进程替换

1.什么是进程程序替换?

execl

原理

其他exec族的函数

替换我们自己的程序


进程控制

fork函数

在此之前,我们基本已经了解到了fork函数,即用它来创建一个子进程,fork函数无参数,返回值类型pid_t,其次fork函数有两个返回值,在子进程中,返回0,创建失败返回-1,在父进程中,给父进程返回子进程的pid。即等于0,子进程,大于0,父进程,-1,创建失败。

这里重点提一下:写时拷贝

通常情况下,父子进程共享一份代码,父进程在不写入时,数据也是共享的,当一个进程要去往里面写入时,便以写时拷贝的方法各自一份副本。

如图,左边为父进程的代码与数据,子进程拷贝父进程的代码数据。右边为当子进程尝试要写入时,此时会重新申请空间,自己拷贝一份数据段副本,再往里面写,并且修改之前的映射关系。

什么是写时拷贝

其中再在父进程创建子进程时,会将自己的数据区(页表)读写权限改为只读,然后再创建子进程。这个过程用户是不知道的,而用户可能会去对某一批数据进行写入,而页表会因为权限此时会出错,操作系统此时会介入进行判断(是因为越界还是权限问题),重新申请内存写入,从而进行写时拷贝。

通过fork函数我们可以让子进程和父进程执行不同的代码,或者一个进程执行不同的程序。

进程终止

首先如何创建一个多进程呢?我们可以利用循环fork创建多个子进程,

 #include<stdio.h>#include<unistd.h>#include<stdlib.h>#define N 10typedef void (*callback_t)();void worker(){int num=10;while(num--){printf("i am a child process,my pid is%d,ppid is%d,num:%d\n",getpid    (),getppid(),num);sleep(1);}}void creatsubproc(int n,callback_t cb){int i;for(i=0;i<n;i++){sleep(1);pid_t p=fork();if(p==0){                                                                  //子进程的工作printf("creat %d chid process\n",i);cb();exit(0);}  }}int main(){creatsubproc(N,worker);//这里传入worker函数,即函数指针sleep(100);return 0;}

首先对于进程的运行,我们不关心,操作系统自己调度,我们创建的多进程,当子进程一个个运行完,就变成僵尸状态了。这里创建子进程时让自己才能恒运行自己worker时,我们可以单独写在外面,通过函数指针的方式,传参,这样以便于我们去修改进程数量以及worker内容。

这是我们再用ps指令监控我们的进程:

while :; do ps ajx| head -1&& ps ajx |grep myproc|grep -v grep;echo "---------------------------";sleep 1;done

 可以看到进程数量不断增多,且慢慢的从s->z状态,一个个终止了。

这里进程终止我们是通过调exit()函数,来实现进程终止,进程终止除了这种方式,也会正常终止,我们可以i用echo $?查看错误码。

对于进程终止,它的原理我们应该清楚,无非就是创建的pcb,页表等,终止时销毁了。可是对于进程终止我们需要了解它的应用:

mian函数的返回值

想了解进程终止的应用,首先我们先了解mian函数的返回值,首先我们知道我们一般写c/c++最后都是返回0,为什么呢?

首先当一个进程跑完,他的情况是怎样的呢?无非有三种情况:代码运行完毕,结果正确;代码运行完毕,结果不正确;代码异常终止;对于父进程,我们只看前两种。

在多进程环境中,父进程想知道子进程运行完结果是怎样的,而且该进程并没有任何的打印信息(我们人也无法观察得出),我们如何得知?

退出码

 对于main函数的返回值,其实就是一个退出码,0就表示的是运行成功-success,非0通常表示运行结果错误--failed,父进程通过子进程返回的退出码,知晓进程运行结果如何。

错误码

对于进程返回0,运行成功,没什么好说的,可是若是非0,运行不正确,但我们并不知道是哪里的问题,只知道进程运行有问题,那么是因为什么原因失败的?此时我们就需要错误码来告诉我们,因为非0表示错误,非0的数字有很多,我们就可以用他们表示不同的出错原因。

系统里会有一套字符串表示错误信息用来对应每个数字,通过错误码就可以知道运行结果及问题所在。而这个字符串错误信息就是strerror(c语言中的时候我们就接触过),我们通过一个循环来看看它的错误码对应的错误信息,并且有多少个:

int mian()
{
//我们也不知道多少,假定200
for(int i=0;i<200;i++)
{printf("%d:%s\n",i,strerror(i));
}
return 0;
}

 而在linux中,可以用$?查看上一次进程(指令)的错误码,?就如同环境变量一样,存放的是错误信息。

这些内置的错误码,如果没有有你想要的的错误说明,你也是可以自定义的,直接定义这样的字符数组

const char*err_string[]={"success","not find","write errorr"}

其下标就是对应的错误码。

对于我们的linux进程的异常,也是有对应的异常信息,对应的异常的错误码如下:

而我们的进程的错误也可以人工向他发出信号给进程从而实现异常:

 exit()

我们在上文知道了,进程直接退出可以调用exit(),那么关于exit到底是什么呢?早在学习c语言的时候,我们也许就接触过了,那么它实际是怎么用的呢?

 可以看到头文件和参数类型,这里的参数其实就是错误码(退出码),作用就是直接引起进程结束。

我们在意一段代码理解exit,进程退出:

 1 #include<stdio.h>2 #include<unistd.h>3 #include<stdlib.h>4 int func()5 {6   printf("call func function done!\n");7   return 11;8 }9 int main()10 {11   func();//调用func函数12    printf("i am a process,pid: %d,ppid: %d\n",getpid(),getppid());13    //直接退出进程 exit(0);14    //对于main函数中直接return返回的是进程的错误码15    //其他函数return,仅代表该函数结束16    //return 21;17    //现在我们不用return 来返回退出码18    //直接调用exit对应的退出码。,其效果等价return                                                                                                     19    exit(21);20 }                                                
~                                                    
~                                                    
~               

我们在这里将他的退出码给21,此时我们运行程序后,在看他的错误码与return是一样的:

现在我们知道只有在main函数中的return 的是退出码,在子函数的return 只是代表返回该函数的返回值,退出该函数。当我们在其他函数中直接调用exit,此时就会直接退出进程,不会运行下面的代码了。

   #include<stdio.h>#include<unistd.h>#include<stdlib.h>int func(){printf("call func function done!\n");// return 11;exit(12);                                                                                                                                            }int main(){func();//调用func函数printf("i am a process,pid: %d,ppid: %d\n",getpid(),getppid());exit(21);}

 可以看到直接退出了进程,退出码为在这个子函数调用的exit的退出码。

除了手册3,man 3 exit 查看到的这个exit,还有man _exit,手册2的一个进程退出函数,用法基本相同,效果也差不多。

 它们有一个区别我们可以用一行代码体现:

printf("hello linu");
printf("hello linu\n");

首先对于exit,在终止进程时会刷新缓冲区,比如第一行代码,在没遇到\n之前,除非强制flush,否则只有等到进程终止此时才会想屏幕打印出来。

对于_exit,还是不加\n的时候。我们运行第一句话,这里没有/n,也没flush,此时进程终止,直接就退出来,什么也没打印,故此_exit终止不刷新缓冲区。

而所谓的缓冲区是绝对不在操作系统里,在我们的c库里,因此_exit与exit是存在这样的区别的。

进程等待

1.什么是进程等待?

首先进程等待就是通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程

2.为什么要进行进程等待?

之前讲过,子进程退出,父进程如果不管不顾,就可能造成 僵尸进程 的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入, kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

1.解决子进程僵尸问题带来的内存泄漏 ---必须的

2.子进程对于自己的任务完成的怎么样父进程需要知道---通过进程等待的方式获取子进程退出的信息(退出码与信号编号)。 

3.如何进程进程等待?

首先聊凭借两个接口:

wait,waitpid:

 对于wait,waitpid都是wait 2手册里的,调用这两个接口,需要包含头文件两个,参数分别为退出码;pid,退出码,选项。

wait的作用是可以帮父进程等待任意一个子进程的退出

waitpid:利用waitpid返回是否成功退出,返回pid退出成功,返回-1退出失败。

我们用一个例子来看看wait的作用:

void worker(){int cnt=5;while(cnt--){printf("i am a process my pid: %d,my ppid:%d\n",getpid(),getppid())    ;sleep(1);}}int main()                                                             {pid_t id=fork();if(id==0){worker();exit(0);}else{//fathersleep(10);pid_t rid=wait(NULL);if(id==rid){//等待成功printf("wait success,pid:%d\n",getpid()); }sleep(10);}return 0;
}

 刚开始五秒,子进程在运行,五秒后遇到exit直接退出(异常退出),此时状态为僵尸状态,我们之前说过,僵尸进程是无法杀掉的,必须要让父进程接收到子进程的退出信息才行,之后继续运行父进程里的,先休眠10秒,做一下区分,然后父进程中,我们直接调用wait,退出码暂时不设置为null,调用wait之后,可以看到僵尸状态的子进程直接没了(子进程被成功回收),只有父进程,在10秒后,父进程结束。

总的概括就是,父进程通过调用wait函数,来获得子进程的退出信息,然后释放子进程,如果没调用wait,获取不到退出信息,此时进程就无法被释放。 

当我们在执行等待时,如果子进程根本就没有退出,父进程就必须在wait时进行阻塞等待,直到自己僵尸进程时候,wait就会自动回收,返回。也就是需要等待子进程运行完成为僵尸进程才能回收。

一般而言,谁先运行我们是不知道的,但是父进程都是最后退出的。

waitpid

wait只能等待当前的进程,没得选,而waitpid可以指定等待的进程。而对于waitdpid我们一般只需要掌握两种参数,首先对于Pid,指定等待进程的Pid,也可以设置为 -1,表示等待任意一个子进程。

参数int *status这里是一个输出型参数(通过函数把这个参数带出来给给操作系统),这里的option默认设置为0,表示的是阻塞等待。

我们将上述的测试再做修改:

 void worker(){int cnt=5;while(cnt--){printf("i am a process my pid: %d,my ppid:%d--%d\n",getpid(),getppid(),cnt);sleep(1);}}int main(){pid_t id=fork();if(id==0){worker();exit(10);}else{//fatherprintf("wait before\n");int status=0;pid_t rid=waitpid(id,&status,0);if(id==rid){//等待成功printf("wait success,pid:%d,%d\n",getpid(),status);}printf("wait after");                                                                                                                             }return 0;}

利用同等方式我们也可以实现进程等待,只不过对于这里的status,为什么时2560我们还是不得而知,实际上status是一个整数,一共32位,低16位:

首先我们在waitpid时不能对status整体使用,且我们可以通过等待完毕后的status的二进制分析得出他的退出码。

其次我们还可以通过有意8位在与上0xFF,得出信号位,用来表示是否收到信号。

对于这里的option除了0,一阻塞方式等待,还有一个状态NOHANG(宏),以非阻塞的方式的等待,

总结:

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候 waitpid 返回收集到的子进程的进程 ID
如果设置了选项 WNOHANG, 而调用中 waitpid 发现没有已退出的子进程可收集 , 则返回 0
如果调用中出错 , 则返回 -1, 这时 errno 会被设置成相应的值以指示错误所在;
参数:
pid
Pid=-1, 等待任一个子进程。与 wait 等效。
Pid>0. 等待其进程 ID pid 相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出)
WEXITSTATUS(status): WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进 程的ID

进程替换

1.什么是进程程序替换?

之前说过,父进程创建的子进程,子进程的代码和数据都是拷贝父进程的,可是我们如何让紫禁城区执行全新的任务呢,访问全新的数据,不在和父进程有瓜葛。且不是再去创建一个子进程。

这是我们需要程序替换:

fork 创建子进程后执行的是和父进程相同的程序 ( 但有可能执行不同的代码分支 ), 子进程往往要调用一种 exec 函数 以执行另一个程序。当进程调用一种exec 函数时 , 该进程的用户空间代码和数据完全被新程序替换 , 从新程序的启动 例程开始执行。调用exec 并不创建新进程 , 所以调用 exec 前后该进程的 id 并未改变。

我们以一个父进程为例

首先了解一下名来进程替换的接口:exec函数族

我们先看下一下execl函数的调用

#include<stdio.h>#include<unistd.h>int main(){//我们知道我们所使用的命令行指令都是一个个程序printf("pid:%d,exec command begin\n",getpid());execl("/usr/bin/ls","ls","-a","-l",NULL);printf("pid:%d,exec command end\n",getpid());return 0;                                                                                                                                           }

我们写一个简单的代码,可以看到如上我们是没有常见任何进程的,但是当我们运行之后

 可以看到我们直接调起了指令ls -a -l。但是后面的那一句话没有打印出来

以同样的方式,我们还调用了top指令。

 #include<stdio.h>#include<unistd.h>int main(){//我们知道我们所使用的命令行指令都是一个个程序printf("pid:%d,exec command begin\n",getpid());execl("/usr/bin/top","top",NULL);                                    printf("pid:%d,exec command end\n",getpid());return 0;}

 从上述的结果看出,我们可以通过语言调用其他程序。

execl

总结上述,那么通过execl函数可以调用其他程序。调用完之后,后面的代码不在运行。

 execl函数就是其中一个可以替换程序的函数:

对于它的参数,第一个path,表示表示替换程序所在路径+文件名,第二个 const char*arg   以及后面的......其实是可变参数列表,都是表示如何使用该指令。

在此之前我们学习到的命令行参数中参数就与这里的可变参数本质上就是同一个。

即第一个参数找到该程序,后面的参数如何执行该程序(与命令行保持一致)。

注意:无论如何去传递参数,末尾一定是以NULL结尾,表示参数传递完毕!

原理

第二个问题,为什么执行完调用的程序,后面没在执行了?

实际上execl执行完就已经完成了程序的替换,我们知道在mm_struct中管理着进程的内存空间:

直接替换源程序的数据与代码,我们源程序的pid等其他属性不变,此时在这个过程中,不用产生新的进程就完成了进程的程序替换。

此时我们再通过多进程再来感受一下进程替换:

 #include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>int main(){pid_t id=fork();if(id==0){//子进程     printf("pid:%d,exec command begin\n",getpid());sleep(1);execl("/usr/bin/ls","ls","-a","-l",NULL);printf("pid:%d,exec command end\n",getpid());}else{                                                               //父进程pid_t rid=waitpid(-1,NULL,0);//父进程来等待子进程if (rid>0){printf("wait succees rid:%d\n",rid);}}return 0;}

 看到结果首先pid是没有变化的,其次还是没有看到command end这句话。

创建子进程的时候,之前我们就说过,子进程与父进程数据共享,代码以写时拷贝的方法各自私有一份,在子进程发生替换时,通过写时拷贝(代码段与数据都重新拷贝),以保持父子的独立性,父进程与子进程不会相互影响。

现在就说一说子进程在替换后,是如何知道我们的代码段该从哪里运行,其次为什么的后面的代码不再执行了?

这个我们之前也提到过(fork创建子进程时运行代码段),其实就是程序计数器,pc指针与 eip会记录函数在运行的过程中,执行到哪一步了,在没替换之前,被记录下来,所以知道从哪里运行,其次,无论是多进程还是单进程,进程替换之后,代码都被替换,eip重新从新的程序开始,下面的代码就不会再执行了,最后被回收。

其他exec族的函数

了解了替换的本质后,我们再来看看exec族的其他函数:

 第一个参数与execl不一样,这里表示的是文件名,即这里的execlp,p指的是path,我们不需要给他完整的路径,他自己会去寻找,给出文件名即可。

  int main(){printf("pid:%d,exec command begin\n",getpid());execlp("ls","ls","-a","-l",NULL);                                    printf("pid:%d,exec command end\n",getpid());return 0;                                    }        

 

再看看exev,这里的vector表示的是vector,技术组,可以看到这里的第二个参数由可变参数变成了字符串数组传参,与我们之前说的命令行参数的形式一样,这里再传入参数时,提前将命令行参数传入数组:

int main(){printf("pid:%d,exec command begin\n",getpid());char*const argv[]={"ls","-a","-l",NULL};                          execv("/usr/bin/ls",argv);printf("pid:%d,exec command end\n",getpid());return 0;}

execvp可以看到这里就是与execv的区别就是不用完整路径(也可以用完整路径)。

int main(){printf("pid:%d,exec command begin\n",getpid());char*const argv[]={"ls","-a","-l",NULL};                          execv("ls",argv);printf("pid:%d,exec command end\n",getpid());return 0;}

替换我们自己的程序

基本掌握了程序替换,对于上述我们都是举例外壳程序的指令,当然我们是可以替换自己的程序:

我们写一个简单的c++代码--打印hello c++,编译形成可执行,通过execl调用我们的程序:

int mian()
{
execl("./myprocc","myprocc",NULL);
return 0;
}                                   

 

当然除了c++,python,汇编程序,其他脚本语言都可以替换,调用。

 

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

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

相关文章

Kibana Dashboard饼图展示keyword子字符串去重统计

日志内容 log.info("请求开始 uri: {} header RequestId:{}", request.getRequestURI(), reqId, request.getHeader("request_id"));操作步骤 进入Dashboard菜单 点击Create Dashboard按钮 点击Create Panel按钮 选择Aggregation based 然后选择Pie饼图 …

开源论道 源聚一堂@COSCon

自2015年以来&#xff0c;开源高峰论坛一直是中国开源年会中的传统亮点项目。本次在COSCon23 大会期间的高峰圆桌会&#xff0c;于2023年10月29日在成都高新区的菁蓉汇召开。 本次高峰圆桌上&#xff0c;我们特别邀请了20 位来自企业&#xff0c;基金会和社区的专家和领袖参加讨…

京东数据分析:2023年10月京东洗衣机行业品牌销售排行榜

鲸参谋监测的京东平台10月份洗衣机市场销售数据已出炉&#xff01; 10月份&#xff0c;洗衣机市场整体销售呈上升走势。鲸参谋数据显示&#xff0c;今年10月&#xff0c;京东平台洗衣机市场的销量为143万&#xff0c;环比增长约23%&#xff0c;同比增长约1%&#xff1b;销售额约…

AI:76-基于机器学习的智能城市交通管理

🚀 本文选自专栏:AI领域专栏 从基础到实践,深入了解算法、案例和最新趋势。无论你是初学者还是经验丰富的数据科学家,通过案例和项目实践,掌握核心概念和实用技能。每篇案例都包含代码实例,详细讲解供大家学习。 📌📌📌在这个漫长的过程,中途遇到了不少问题,但是…

物联网AI MicroPython学习之语法 ustruct 打包和解压原始数据类型

学物联网&#xff0c;来万物简单IoT物联网&#xff01;&#xff01; ustruct 介绍 ustruct提供打包和解压原始数据类型的功能。 默认情况下&#xff0c;C类型以机器的本机格式和字节顺序表示&#xff0c;并在必要时通过跳过填充字节来正确对齐&#xff08;根据C编译器使用的规…

STL常用库函数复习

文章目录 pairvectorliststackqueuequeuepriority_queuequeue双端队列 set✨set集合✨multiset 多重集合了解&#xff1a;unordered_set 无序集合 map&#x1f31f;map几乎不用&#xff1a;multimap一般不用&#xff1a;undered_map pair utility示例 #include <iostream&…

【教3妹学编程-算法题】 在树上执行操作以后得到的最大分数

3妹&#xff1a;2哥&#xff0c;今日都立冬了&#xff0c; 可是天气一点都不冷。 2哥 : 立冬了&#xff0c;晚上要不要一起出去吃饺子&#xff1f;&#x1f95f; 3妹&#xff1a;好呀好呀&#xff0c;2哥请吃饺子喽 2哥 : 歪歪&#xff0c;我说的是一起出去吃&#xff0c;没说我…

以 Kubernetes 原生方式实现多集群告警

作者&#xff1a;向军涛、雷万钧 来源&#xff1a;2023 上海 KubeCon 分享 可观测性来源 在 Kubernetes 集群上&#xff0c;各个维度的可观测性数据&#xff0c;可以让我们及时了解集群上应用的状态&#xff0c;以及集群本身的状态。 Metrics 指标&#xff1a;监控对象状态的量…

C++day6作业

1.思维导图 2.编程题&#xff1a; 以下是一个简单的比喻&#xff0c;将多态概念与生活中的实际情况相联系&#xff1a; 比喻&#xff1a;动物园的讲解员和动物表演 想象一下你去了一家动物园&#xff0c;看到了许多不同种类的动物&#xff0c;如狮子、大象、猴子等。现在&am…

[autojs]用户界面GUI编程

用户界面: UI视图: View attr(name, value)attr(name)whidgravitylayout_gravitymarginmarginLeftmarginRightmarginTopmarginBottompaddingpaddingLeftpaddingRightpaddingToppaddingBottombgalphaforegroundminHeightminWidthvisibilityrotationtransformPivotXtransformPivo…

5-爬虫-打码平台、打码平台自动登录打码平台、selenium爬取京东商品信息、scrapy介绍安装、scrapy目录结构

1 打码平台 1.1 案例 2 打码平台自动登录打码平台 3 selenium爬取京东商品信息 4 scrapy介绍安装 5 scrapy目录结构 1 打码平台 # 1 登录某些网站&#xff0c;会有验证码---》想自动破解-数字字母&#xff1a;python模块&#xff1a;ddddocr-计算题&#xff0c;成语题&#xf…

如何评价现在的CSGO游戏搬砖市场

如何评价现在的csgo市场&#xff1f; 其实整个搬砖市场&#xff0c;现在已经变得乌烟瘴气&#xff0c;散发着“恶臭”。我个人非常鄙视那些虚有其表&#xff0c;大小通吃的做法&#xff0c;那些甚至连搬砖数据都看不懂的人&#xff0c;也出来吹嘘着“实力强大&#xff0c;经验丰…

本地生活新赛道-视频号团购怎么做?

目前有在做实体行业的商家一定要看完&#xff0c;只要你进入了这个本地生活新的赛道&#xff0c;那你的生意自然会源源不断&#xff0c;那这个赛道又是什么呢&#xff1f; 这就是十月份刚刚上线的视频号团购项目&#xff0c;开通团购之后&#xff0c;就可以通过发短视频&#…

深度学习pytorch之hub模块

pytorchhub模块里面有很多模型 https://pytorch.org/hub/ github网址&#xff1a;https://github.com/pytorch/pytorch import torch model torch.hub.load(pytorch/vision:v0.10.0, fcn_resnet50, pretrainedTrue) # or # model torch.hub.load(pytorch/vision:v0.10.0, fc…

Linux C语言进阶-D15递归函数和函数指针

递归函数 指一个函数的函数体中直接或间接调用了该函数本身 执行过程分为两个过程&#xff1a; 递推过程&#xff1a;从原问题出发&#xff0c;按递归公式递推从未知到已知&#xff0c;最终达到递推终止条件 回归阶段&#xff1a;按递归终止条件求出结果&#xff0c;逆向逐步…

2023最新版本 FreeRTOS教程 -10-事件组(通过5种情况快速上手)

事件组对应单个事件触发或多个事件同时触发的场景 创建事件组函数 EventGroupHandle_t xEventGroupCreate( void );删除事件组函数 void vEventGroupDelete( EventGroupHandle_t xEventGroup )设置事件 在任务中使用xEventGroupSetBits() 在中断中使用xEventGroupSetBits…

【Proteus仿真】【51单片机】水质监测报警系统设计

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真51单片机控制器&#xff0c;使用按键、LED、蜂鸣器、LCD1602、PCF8591 ADC、PH传感器、浑浊度传感器、DS18B20温度传感器、继电器模块等。 主要功能&#xff1a; 系统运行后&…

商业计划书PPT怎么做?这个AI软件一键在线生成,做PPT再也不求人!

商业计划书是一份重要的书面文件&#xff0c;它通常被用作商业估值、筹资和进一步扩大业务的基础。一个好的商业计划书能够让团队向投资者、潜在客户和业务合作伙伴展示其企业的价值&#xff0c;并且清楚地阐述企业的产品或服务能够如何满足市场需求。作为商业计划书的重要组成…

HuggingFace的transfomers库

pipeline from transformers import pipelineclassifier pipeline("sentiment-analysis")#自动下载模型和tokenizer classifier("We are very happy to show you the &#x1f917; Transformers library.")#[{label: POSITIVE, score: 0.9998}] #输入多…

C# OpenCvSharp 玉米粒计数

效果 项目 代码 using OpenCvSharp; using System; using System.Drawing; using System.Text; using System.Windows.Forms;namespace OpenCvSharp_Demo {public partial class frmMain : Form{public frmMain(){InitializeComponent();}string fileFilter "*.*|*.bmp;…