当前位置: 首页 > news >正文

【Linux】进程控制--进程创建/进程终止/进程等待/进程程序替换/简易shell实现

文章目录

  • 一、进程创建
    • 1.fork函数
    • 2.fork函数返回值
    • 3.写时拷贝
    • 4.fork常规用法
    • 5.fork调用失败的原因
  • 二、进程终止
    • 1.进程退出码
    • 2.进程退出场景
    • 3.进程常见退出方法
  • 三、进程等待
    • 1.为什么要进行进程等待
    • 2.如何进行进程等待
      • 1.wait方法
      • 2.waitpid方法
      • 3.获取子进程status
      • 4.进程的阻塞等待方式与非阻塞等待方式
      • 5.进程等待总结
  • 四、进程程序替换
    • 1.创建子进程的目的
    • 2.什么是进程程序替换
    • 3.进程程序替换的原理
    • 4.如何进行进程程序替换
      • (1) 替换函数
      • (2) 函数命名理解
      • (3)函数的使用
  • 五、实现一个简易的shell
    • 1.shell的初步实现
    • 2.什么是当前路径
    • 3.内建命令/外部命令
    • 4.shell 完整代码

一、进程创建

1.fork函数

fork函数是Linux中的一个非常重要的系统调用函数,它用于在当前进程下创建一个新的进程,新进程是当前进程的子进程,我们可以使用man 2号手册来查看fork函数:

在这里插入图片描述

// 头文件
#include <unistd.h>// 创建一个子进程
pid_t fork(void);//返回值:子进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做以下任务:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

在这里插入图片描述

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以

开始它们自己的旅程,看如下程序

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>int main()
{pid_t id = fork();if(id == -1){printf("fork fail\n");exit(-1);}else if(id == 0){// childwhile(1){printf("我是子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);sleep(1);}}else{// parentwhile(1){printf("我是父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);sleep(1);}}return 0;
}

在这里插入图片描述

结论:fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器

决定。

一个小技巧我们在编写Makefile的时候,目标文件的依赖方法中,可以使用" @ " 来表示形成的目标文件,即依赖关系中 " : " 左边的内容,用 " @"来表示形成的目标文件,即依赖关系中":"左边的内容,用" @"来表示形成的目标文件,即依赖关系中":"左边的内容,用"^“表示目标文件的依赖文件,即依赖关系中”:"右边的内容

在这里插入图片描述

2.fork函数返回值

fork函数有两个返回值,子进程返回0,父进程返回的是子进程的pid

我们学过C/C++之后知道,一个函数的返回值最多只有一个,那么我们如何理解fork函数有两个返回值呢

我们知道,fork函数是一个系统调用,即fork函数是操作系统为我们提供的一个操作接口,所以fork函数也是由操作系统实现的,所以当我们调用fork函数时,其实是操作系统帮我们创建子进程。一个函数在正常的执行的情况下,函数return之前函数的主体功能肯定已经被执行完了,对于fork函数来说,fork函数的作用是创建子进程,所以fork函数在return之前就已经创建了子进程,那么此时就存在两个进程。既然存在两个进程,那么fork函数也就会被返回两次,因为每一个进程都会return,所以fork函数有两个返回值

我们如何理解fork返回之后,给父进程返回子进程的pid,给子进程返回0呢

因为一个父进程可能有多个子进程,而一个子进程只能有一个父进程,父进程需要子进程的pid来判别不同的子进程,而子进程则不需要判别父进程,直接调用getppid即可获得父进程的pid

如何理解同一个id值,怎么可能会保存不同的id值,让if 和 else if 同时执行

我们知道,子进程会拷贝父进程的PCB,数据结构以及页表,但是当一个进程对其数据进程写入的时候,就会发生写时拷贝,改变页表的映射关系,在一个新的空间存储数据,fork函数返回,而返回的本质就是写入,所以,谁先返回就谁先写入,因为进程具有独立性,发生写时拷贝,所以可以if 和else if同时执行

3.写时拷贝

我们看下面的程序:

#include <stdio.h>
#include <unistd.h>int global_val = 100;int main()
{pid_t id = fork();if(id < 0){printf("fork error\n");return 1;}else if(id == 0){int cnt = 0;while(1){printf("我是子进程,pid:%d,ppid:%d | global_val:%d,&global:%p\n", getpid(),getppid(),global_val,&global_val);sleep(1);cnt++;if(cnt == 10){printf("子进程已经更改了全局的变量啦.....\n");global_val = 300;}}}else{while(1){printf("我是父进程,pid:%d,ppid:%d | global_val:%d,&global:%p\n", getpid(),getppid(),global_val,&global_val);sleep(2);}}return 0;
}

在这里插入图片描述

我们发现,子进程和父进程中global_val变量的地址相同,但是值却不相同,我们知道,操作系统会每一个进程都创建一个进程地址空间和页表,然后通过页表将地址空间映射到物理内存

对于父进程来说,父进程和子进程共享代码和数据,但是为了保证进程的独立性,当其中一个进程需要修改数据的时候,就会发生写时拷贝–操作系统会在物理内存重新开辟一块空间,然后将原空间中的数据拷贝到新的空间,然后在修改映射关系,最后再让进程修改对应的数据

所以表面省父子进程的global_val的 地址相同,但是这只是虚拟地址相同,而物理地址并不相同,所以父子进程的global_val的值并不相同,对于接收fork函数返回值的变量id来说也是如此,先进行return的进程会对id进行写时拷贝,所以对于父子进程来说,id的值也不相同

在这里插入图片描述

4.fork常规用法

fork函数一般用于下面两种场景:

1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。

2.一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

5.fork调用失败的原因

有如下两种原因可能会导致fork函数调用失败:

1.系统中有太多的进程

2.实际用户的进程数超过了限制

我们可以写一个死循环创建进程的程序来测试我们当前的操作系统能够创建多少个进程:

#include <stdio.h>
#include <unistd.h>int main()
{int cnt = 0;while(1){int ret = fork();if(ret < 0){printf("fork error!, cnt: %d\n", cnt);break;}else if(ret == 0){//childwhile(1) sleep(1);}//partentcnt++;}return 0;
}

上面的代码可能会导致服务器或者虚拟机直接挂掉,所以我这里就不进行测试了,有兴趣的小伙伴可以进行测试一下。

二、进程终止

1.进程退出码

我们运行一个进程是为了让进程为我们完成某一项任务,既然是为了完成一个任务,那么我们就可能会关心该进程完成任务的情况,所以就需要对任务的执行结果进行判定,此时就需要用到进程退出码,进程退出码的作用就是标定一个进程的执行结果是否正确,不同的进程退出码表示不同的执行结果,一般来说,进程退出有三种情况:

1.进程退出(代码跑完),结果正确,此时return 0

2.进程退出(代码跑完),结果不正确,此时 return !0

3.代码没跑完,程序异常了,此时退出码无意义

对于 !0 来说,不同的数字又对应着不同的错误码,我们可以使用系统提供的退出码的映射关系,也可以自己设定不同的退出码所对应的错误信息,我们可以使用C语言提供的strerror函数打印出系统提供的错误码的映射关系:

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

在这里插入图片描述

在Linux中,存在一个变量 " ? " -该变量中始终保存着最近一个进程执行完成时的退出码,我们可以使用" echo $?"来查看最近一个进程的退出码:

在这里插入图片描述

我们可以看到,我们再次输入 "echo #?"指令的时候,打印的值为0,这是由于 echo本身也是一个可执行程序,我们使用 echo查看 ? 时 echo也会被运行,所以我们后面再次查看 $? 时,得到的结果为0

2.进程退出场景

进程退出时一个有三种情景:

1.代码运行完毕且结果正确–此时退出码为0

2.代码运行完毕但是结果错误-此时退出码为非0

3.代码异常终止–此时退出码无意义

3.进程常见退出方法

进程退出有以下三种方法:

1.main函数return返回

2.调用exit终止程序

3.调用_exit终止程序

我们平时最常用的就是通过main函数return返回退出程序,但是其实我们也可以通过库函数exit和系统调用_exit直接终止程序

库函数exit

头文件:stdlib.h
函数原型:void exit(int status);status:status 定义了进程的终止状态,父进程通过wait来获取该值函数功能:终止程序

在这里插入图片描述

在这里插入图片描述

我们可以看到,exit会将我们的进程直接终止,无论程序代码是否执行完毕

系统调用 _exit

头文件:unistd.h函数原型:void _exit(int status);status:status定义了进程的终止状态,父进程通过wait来获取该值函数功能:终止进程

在这里插入图片描述

在这里插入图片描述

【注意】

参数:status 定义了进程的终止状态,父进程通过wait来获取该值

说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值255

exit 和 -exit 的区别

exit 终止进程,会主动刷新缓冲区,_exit终止进程,不会刷新缓冲区

我们以下面的例子来进行说明:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main()
{printf("process is running");exit(1);// _exit(1);printf("process is running done\n");return 0;
}

在这里插入图片描述

在这里插入图片描述

我们可以看到,exit 终止进程,会主动刷新缓冲区,_exit终止进程,不会刷新缓冲区,分析如下:

1.由于exit是C语言库函数,而_exit是系统调用,所以可以肯定的是exit的底层是_exit函数,exit是_exit的封装

2.由于计算机体系结构的限制,CPU之间和内存交互,所以数据会先被写入到缓存区,待缓存区刷新时才被打印到显示器上,而上面的程序中,我们没有使用"\n"进行缓冲区的刷新,可以看到,exit最后打印了"process id running",而_exit什么也没有打印,所以exit在终止程序后会刷新缓冲区,而_exit终止程序后不会刷新缓冲区

3.由于exit的底层是_exit,而_exit并不会刷新缓冲区,可以反映出缓冲区不在操作系统内部,而是在用户空间

进程退出不仅有正常的退出,还有不正常的退出,比如Ctrl C终止进程,或者程序中除0,野指针,空指针的解引用等问题,程序就会异常退出

在这里插入图片描述

exit最后也会调用exit, 但在调用exit之前,还做了其他工作:

1.执行用户通过 atexit或on_exit定义的清理函数。

2.关闭所有打开的流,所有的缓存数据均被写入

3.调用_exit

在这里插入图片描述

三、进程等待

1.为什么要进行进程等待

为什么要进行进程等待呢,有如下原因:

我们创建一个进程的目的是为了让其帮我们完成某种任务,而既然是完成任务,进程在结束前就应该返回任务执行的结果,供父进程或者操作系统进行读取,所以,一个进程在退出的时候,不能立即释放其全部的资源–对于进程的代码和数据,操作系统可以释放,因为该进程已经不会再被执行了,但是该进程的PCB应该被保留下来,因为PCB中存放着该进程的各种状态的代码,其中就包括退出状态代码。对于父子进程来说,当子进程退出后,如果父进程不对子进程的退出状态进行读取,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。从而就会造成内存的泄漏

所以,我们需要父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息,并让操作系统回收子进程的资源(释放子进程的PCB)

进程等待的本质

我们知道,子进程的退出信息是存放在子进程的task_struct中的,所以进程等待的本质就是从子进程task_struct中读取退出信息,然后保存到对应的变量中取

在这里插入图片描述

2.如何进行进程等待

1.wait方法

我们可以通过wait系统调用来进行进程等待

在这里插入图片描述

头文件: sys/types.h  sys/wait.h函数原型:pid_t wait(int* status)status:输出型参数,获取子进程退出状态返回值:成功返回被等待进程的pid,失败返回-1

我们以以下的例子来说明wait的使用:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id == 0){//子进程int cnt = 5;while(cnt){printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);}exit(0); //进程退出}sleep(15);int status = 0;pid_t ret = wait(&status);if(ret > 0){printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status>>8)&0xFF);}sleep(5);return 0;
}

我们可以使用一个监控脚本来检测子进程从创建到终止到被父进程回收过程中状态的变化:

while :; do ps axj | head -1 && ps axj | grep mytest | grep -v grep; sleep 1; done

在这里插入图片描述

我们可以看到,最开始父子进程都处于睡眠状态,之后子进程运行5s之后,此时由于父进程还要休眠10s,所以没有对子进程进行回收,所以子进程变成僵尸进程,10s过后,父进程使用wait系统调用对子进程进行进程等待,所以子进程由僵尸状态变成了死亡状态

2.waitpid方法

我们也可以使用waitpid来进行进程等待

在这里插入图片描述

头文件:sys/types.h sys/wait.h函数原型:pid_t waitpid(pid_t pid,int* status,int option);pid :pid=1,等待任意一个子进程,与wait等效,pid > 0,等待其进程与pid相等的子进程;status:输出型参数,获取子进程退出状态,不关心则可以直接设置为NULLoption:等待方式,option = 0 -> 阻塞等待,option = WNOHANG -> 非阻塞等返回值:waitpid调用成功时返回被等待进程的pid;如果设置了WNOHANG,且waitpid发现没有已退出的子进程可收集,则返回0,调用失败则返回-1

我们以以下的例子来说明waitpid的使用:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id == 0){//子进程int cnt = 5;while(cnt){printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);}exit(12); //进程退出}// 父进程sleep(10);int status = 0; // 不是被整体使用的,有自己的位图结构pid_t ret = waitpid(id, &status, 0);if(ret > 0){printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status>>8)&0xFF);}sleep(5);return 0;
}

在这里插入图片描述

我们可以看到,waitpid和wait还是有很大区别,waitpid可以传递id来指定等待特定的子进程,也可以指定option来指明等待方式

【总结】

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。

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

如果不存在该子进程,则立即出错返回

3.获取子进程status

我们在上面的程序中,打印sig number和child exit code的时候,打印的格式分别为status & 0x7F), (status>>8)&0xFF);这是由于status的位图结构决定我们这是使用的:

我们知道,wait和waitpid都有一个参数该参数是一个输出型参数,由操作系统填充,如果传递的参数是 NULL,则表示不关心子进程的退出状态的信息,否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程

status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图,其中,我们只需要研究status的低16比特位

在这里插入图片描述

我们可以看到,status低两个字节的内容被分成了两个部分–第一个字节的前七位表示退出信号,最后一位表示core dump标志,第二个字节表示退出的状态,退出状态即表示进程退出时的退出码

对于正常退出的程序来说,退出信号和core dump都标志为0,退出状态等于退出码,对于异常终止的程序来说,退出信号为不同终止原因对应的数字,此时退出状态就没有意义

所以status正确的读取方式如下:

printf("exit signal:%d,exit code:%d\n",(status & 0x7f),(status>>8 & 0xff));

其中,status按位与上0x7f表示保留低七位,其余九位全部置为0,从而得到退出信号

status右移8位得到退出状态,再按位与上0xff是为了得到防止右移时高位补1的情况

WIFEXITED与WEXITSTATUS宏

Linux提供了WIFEXITED与WEXITSTATUS宏来帮助我们获取status中的退出状态和退出信号,而不再需要我们自己执行按位操作

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

4.进程的阻塞等待方式与非阻塞等待方式

waitpid函数的第三个参数用于指定父进程的等到方式

在这里插入图片描述

其中,option代表阻塞等待方式,option为WNOHANG代表非阻塞等待

阻塞式等待即当父进程执行到waitpid函数时,如果子进程还没有退出,那么父进程就只能阻塞在waitpid函数,直到子进程退出,父进程通过waitpid读取退出信息后才能执行后面的代码

而非阻塞式等待则是,当父进程执行到waitpid函数时,父进程会直接读取子进程的状态并返回,然后接着执行后面的代码,不会等待子进程退出

轮询

轮询是指父进程在非阻塞的状态的前提下,以循环的方式不断对子进程进行进程等待,只带子进程退出

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>#define NUM 10typedef void (*func_t)(); //函数指针func_t handlerTask[NUM];//样例任务
void task1()
{printf("handler task1\n");
}
void task2()
{printf("handler task2\n");
}
void task3()
{printf("handler task3\n");
}void loadTask()
{memset(handlerTask, 0, sizeof(handlerTask));handlerTask[0] = task1;handlerTask[1] = task2;handlerTask[2] = task3;
}int main()
{pid_t id = fork();// fork返回-1 直接断言断死assert(id != -1);if(id == 0){//childint cnt = 10;while(cnt){printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);}exit(10);}loadTask();// parentint status = 0;while(1){pid_t ret = waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞-> 子进程没有退出, 父进程检测时候,立即返回if(ret == 0){// waitpid调用成功 && 子进程没退出//子进程没有退出,我的waitpid没有等待失败,仅仅是监测到了子进程没退出.printf("wait done, but child is running...., parent running other things\n");for(int i = 0; handlerTask[i] != NULL; i++){handlerTask[i](); //采用回调的方式,执行我们想让父进程在空闲的时候做的事情}}else if(ret > 0){// 1.waitpid调用成功 && 子进程退出了printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);break;}else{// waitpid调用失败printf("waitpid call failed\n");break;}sleep(1);}return 0;
}

在这里插入图片描述

5.进程等待总结

1.为了读取子进程的退出结果以及回收子进程的资源,我们需要进程等待

2.进程等待的本质是父进程从子进程的task_struct 中读取退出信息,然后保存到status中

3.我们可以通过wait和waitpid系统调用获取退出信息,完成进程等待

4.status参数是一个输出型参数,父进程通过wait/waitpid函数将子进程的退出信息写入到status中

5.status以位图的方式存储,包括退出状态和退出信号,如果退出信号不为0,那么退出状态就没有意义

6.我们可以使用系统提供的宏 WIFEXITED和WEXITSTATUS来分别获取status中的退出状态和退出信号

7.进程等待方式分为阻塞式等待方式和非阻塞式等待方式,阻塞式等待方式用0来标识,非阻塞式等待方式用宏WONHANG来进行标识

8.由于非阻塞式等待不会等待子进程退出,所以我们需要使用轮询的方式来不断的获取子进程的退出信息

四、进程程序替换

1.创建子进程的目的

创建子进程由两个目的:

1.想让子进程执行父进程代码的一部分,执行父进程对应的磁盘代码中的一部分

2.想让子进程执行一个全新的程序,让子进程想办法,加载磁盘上指定的程序,执行新程序的代码和数据

2.什么是进程程序替换

对于创建子进程的第二个目的–让子进程来执行一个不同的程序就是程序替换

进程程序替换是指父进程使用fork函数来创建子进程后,子进程通过调用exec系列的函数来执行另一个程序,当进程调用某一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,然后执行新的程序

但是原进程的task_struct和mm_struct以及进程的id后不会改变,页表的映射关系可能会发生改变,所以调用exec系列函数时并不会创建一个新的进程,而是让原来的进程去执行另外一个新程序的代码和数据

3.进程程序替换的原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数

以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动

例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。所以进程程序替换就是用新的程序的代码和数据区替换原进程物理内存中的代码和数据。

在这里插入图片描述

4.如何进行进程程序替换

(1) 替换函数

Linux提供了一系列的exec函数来实现进程程序替换,其中包括六个库函数和一个系统调用

在这里插入图片描述

在这里插入图片描述

我们可以看到,实现进程程序替换的系统调用函数就只有一个–execve,其他的一系列的exec函数都是为了满足不同的替换场景而对execve系统调用进行的封装,其底层还是调用execve

六个库函数如下

#include <unistd.h>extern char **environ;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[]);

这些函数一旦调用成功,就代表着原程序的代码和数据已经被新的程序替换了,也就是说,原来的程序的后续的语句就都不会再被执行了,所以exec函数调用成功之后没有返回值,因为该返回值接收已经没有意义了。只有exec函数调用失败,原程序可以继续往下执行时,exec返回才有意义

【总结】这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。如果调用出错则返回-1所以exec函数只有出错的返回值而没有成功的返回值

(2) 函数命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH路径下搜索文件,即对于替换Linux指令相关程序时不需要我们带路径

e(env) : 表示自己维护环境变量

在这里插入图片描述

(3)函数的使用

我们想要执行一个程序,首先需要找到该可执行程序,二是指定程序执行的方式,即按照怎样的方式去执行,对于exec函数来说,'p’和非’p’用来找到程序,'l’和’v’用来指定程序执行方式,'e’用来指定环境变量

execl && execlp

exec函数的使用其实很简单,第一个参数为我们需要替换的程序的路径,如果该程序在PATH环境中,且exec函数带有"p",我们可以不带理解,只写函数名

我们以linux指令"ls"为例,我们知道,ls是Linux中"usr/bin"目录下的一个可执行程序,且该程序处于PATH环境变量中,那么如果我们需要替换该程序,exec函数的第一个参数如下:

execl("/usr/bin/ls",...); // 需要带路径
execlp("ls",..); // 可以不带路径

我们需要注意的是,带"p"的exec函数可以不带;路径的前提是被替换程序处于PATH环境变量中,如果没有处于PATH环境变量中,那么我们 仍然需要带路径

第二个参数是我们如何执行我们的程序,这里我们需要记住一点:在Linux命令行中给程序如何执行我们就如何传递参数。需要注意的是,命令行中多个指令是以空格为分割的一整个字符串,而exec函数中我们需要对不同的选项进行分割,即每一个选项都要单独分为一个字符串,所以可以看到exec函数中存在可变参数列表"…",同时,我们现在需要将最后一个可变参数设置为NULL,表示参数传递完毕

// 命令行中怎么传递就怎么传递  ls -a -l
execl("/usr/bin/ls","ls","-a","l",NULL);
execlp("ls","ls","-a","l",NULL);

我们需要注意的是,Linux中ls 其实是使用alise命令设置别名的,所以我们执行ls的时候默认带了"-color=auto"选项,它让不同类型的文件带有不同的颜色

所以我们在ls进程程序替换时如果我们想要让不同类型的文件表现为不同的颜色的话,那么我们需要显式的传递"-color=auto"选项

execl("/usr/bin/ls","ls","-a","-l","--color=auto",NULL);
execlp("ls","ls","-a","-l","--color=auto",NULL);

下面我们以一个具体的例子来演示如何进行进程程序替换:

#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 == -1){perror("fork");exit(1);}else if(id == 0){printf("pid:%d,child process running..\n",getpid());int ret = execl("/usr/bin/ls","ls","-a","-l","--color=auto",NULL);if(ret == -1){printf("process exec fail\n");exit(1);}printf("pid:%d,child process done..\n",getpid());return 0;}int status;pid_t ret = waitpid(id, &status, 0);if(ret == -1){perror("waitpid");return 1;}else{printf("wait success: exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);}return 0;
}

在这里插入图片描述

我们可以看到,我们在命令行当使用"ls -a -l"和我们使用进程程序替换得到的结果是一样的

execv && execvp

exec函数中"v"代表参数使用数组的形式进行传递–argv是一个指针数组,数组里面的每一个元素都是指针,每一个指针都指向一个参数(字符串),同样,最后一个元素指向NULL,代表参数传递完毕

我们还是以ls指令为例来进行演示:

char* argv[]={(char*)"ls",(char*)"-a",(char*)"-l",(char*)"--color=auto",NULL  };
execlv("/usr/bin/ls",argv);                                                               
execvp("ls",argv);      

由于"ls","-a"等字符串都是常量字符串,而argv里面的参数是char* const而不是const char* 的,所以我们这里需要强转一下,不强转问题也不大

在这里插入图片描述

execle && execvpe

exec函数中的"e"代表环境变量-和argv一样,envp也是一个指针数组,数组里面的每一个元素都是一个指针,指向一个环境变量(字符串),我们可以显式的初始化envp来传递我们自定义的环境变量,但是这也代表了我们放弃了字体默认的环境变量

char *const envp_[] = {(char*)"MYENV=11112222233334444",NULL};
execle(".mybin","./mybin",NULL,envp);

mybin.c

#include <stdio.h>
#include <stdlib.h>int main()
{// 系统就有printf("PATH:%s\n", getenv("PATH"));printf("PWD:%s\n", getenv("PWD"));// 自定义printf("MYENV:%s\n", getenv("MYENV"));printf("我是另一个C程序\n");printf("我是另一个C程序\n");printf("我是另一个C程序\n");printf("我是另一个C程序\n");printf("我是另一个C程序\n");printf("我是另一个C程序\n");printf("我是另一个C程序\n");printf("我是另一个C程序\n");printf("我是另一个C程序\n");return 0;
}

myexec.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>int main(int argc, char *argv[])
{printf("process is running...\n");pid_t id  = fork();assert(id != -1);if(id == 0){execle("./mybin", "mybin", NULL, envp_); //自定义环境变量exit(1); //must failed}int status = 0;pid_t ret = waitpid(id, &status, 0);if(ret>0) printf("wait success: exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
}

在这里插入图片描述

我们可以看到,这里我们只获取到了自定义的环境变量MYENV,而系统的环境变量PATH和PWD则是没有被获取到

我们可以通过传递environ来获取系统的环境变量

if(id == 0)
{extern char** environ;execle("./mybin", "mybin", NULL, environ); //自定义环境变量exit(1); //must failed
}

在这里插入图片描述

但是这个时候我们又不能够获取我们自定义的环境变量,那么我们该如何同时获取到自定义环境变量和系统环境变量呢,这个时候我们可以使用putenv函数将自定义环境变量导入到系统环境变量中,然后通过传递环境变量environ来实现

在这里插入图片描述

putenv((char*)"MYENV=4443332211"); //将指定环境变量导入到系统中 environ指向的环境变量表
execle("./mybin", "mybin", NULL, environ); //实际上,默认环境变量你不传,子进程也能获取

在这里插入图片描述

五、实现一个简易的shell

1.shell的初步实现

我们实现一个简易的命名行解释器大概需要分为一下几个步骤:

1.输出提示符,即我们平时写指令的左边的提示符

2.从终端获取命令进行指令输入

3.解析输入的命令

4.创建子进程

5.进程程序替换

6.进程等待

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>#define NUM 1024 // 一个指令的最大长度
#define OPT_NUM 64  // 一个指令的最多选项char lineCommand[NUM];  // 保存输入命令的数组
char* myargv[OPT_NUM];  // 保存选项的数组int main()
{while(1){// 输出提示符printf("[用户名@主机名 当前路径]$");fflush(stdout);// 从键盘(stdin)获取指令输入char* s = fgets(lineCommand,sizeof(lineCommand)-1,stdin);//最后一个位置来保存极端场景下的\0if(s == NULL){perror("fgets");exit(1);}lineCommand[strlen(lineCommand)-1] = '\0', //消除命令行中最后的换行符// 将输入的字符串解析为说个字符串存放到myargv数组中,即字符串切割myargv[0] = strtok(lineCommand," ");int i = 1;while(myargv[i++] = strtok(NULL," "));// 创建子进程pid_t id = fork();if(id == -1){perror("fork");exit(1);}else if(id == 0){// 子进程进行进程程序替换execvp(myargv[0],myargv);exit(1);}else{int status = 0;pid_t ret = waitpid(id, &status,0);if(ret == -1){perror("waitpid");exit(1);}   }}return 0;
}

在这里插入图片描述

这样我们就完成了Linux中的一些基本指令了,但是我们发现,我们使用ls的时候没有颜色的功能,我们可以在程序中对ls指令进行单独的判断,然后手动的为其加上"–color=auto"选项

if(myargv[0] != NULL && strcmp(myargv[0],"ls") == 0)
{myargv[1++] = (char*)"--color=auto";
}

在这里插入图片描述

2.什么是当前路径

我们在运行我们上面的程序的时候会发现一个问题,当我们使用cd更换路径的时候,再使用pwd命令还是显示我们原来的路径

在这里插入图片描述

我们在解决这个问题之前,我们需要先理解什么是当前路径:

在这里插入图片描述

我们可以看到,在test程序运行起来之后,在系统中一共有两个路径,一个exe路径是指test可执行程序在磁盘中的路径,而cwd(current working directory) 为当前进程的工作目录,就我们平常所说的当前路径

在Linux中,我们可以使用chdir这个系统调用来改变进程的工作目录:

在这里插入图片描述

在这里插入图片描述

在理解什么是当前进程的工作目录之后,我们就可以解释为什么我们的shell执行cd命令后目录不会改变了

myshell是通过创建子进程的方式去执行命令行中的各种指令的,也就是说,cd命令是由子进程去执行的,那么自然被改变的也是子进程的工作目录,父进程的工作目录则不会改变

而我们使用pwd指令来查看当前路径时,cd指令对应的子进程已经执行完毕退出了,此时myshell又会给pwd创建一个新的进程,且这个子进程的工作目录和父进程的工作目录相同,所以pwd打印出来的工作目录不会改变

我们想要解决这个问题,就需要使用chdir将父进程的工作目录修改为指定的目录即可,所以这里我们呀需要对指令进行单独的判断

// cd 改变父进程的路径
if(myargv[0] != NULL && strcmp(myargv[0],"cd") == 0)
{if(myargv[1] != NULL){chdir(myargv[1]);// myargv[1]中保存着指定路径}continue;  // 下面的语句不需要在继续执行了,以为你cd的目的已经达到了
}

在这里插入图片描述

3.内建命令/外部命令

Linux中的命令一共分为两种–内建命令和外部命令

内建命令是shell程序的一部分,其功能在bash源代码中,不需要派生子进程来执行,也不需要借助外部程序文件来运行,而是由shell进程本身内部的逻辑来完成,外部命令则是通过创建子进程,然后进行进程程序替换,运用外币程序文件等方式来完成

我们可以使用type命令来区分Linux中的内置命令为外部命令

在这里插入图片描述

我们对cd指令就是以内置命令的方式来进行处理的–myshell遇到cd命令时,由自己直接改变工作目录,处理完毕直接continue,而不会通过创建子进程的方式来完成,不过对于pwd我们没有单独处理成内建命令

同时我们发现echo也是一个内建命令,这也解释了为什么"echo$ 变量"可以查看本地变量和"echo$?"可以获取最近的一个进程的退出码,原因如下:

本地变量只是在当前进程有效,但是使用echo查看本地变量时,shell并不会创建子进程,而是直接在当前进程中查找,所以可以查看本地变量

shell可以通过进程等待的方式获取一个子进程的退出状态,然后将其保存在?变量中,当命令行输入"echo$?"时,直接输出?中的内容,然后将?置为0(echo正常退出的退出码),也不需要创建子进程

所以我们也可以在我们的shell程序加入echo命令了:

int  lastCode = 0; // 保存退出码
int  lastSig = 0; //保存退出信号
if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
{if(strcmp(myargv[1], "$?") == 0){printf("%d, %d\n", lastCode, lastSig);}else{printf("%s\n", myargv[1]);}continue;
}// fork之后添加的内容
lastCode = ((status >> 8) & 0xff);
lastSig = (status & 0x7f);

在这里插入图片描述

4.shell 完整代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>#define NUM 1024 // 一个指令的最大长度
#define OPT_NUM 64  // 一个指令的最多选项char lineCommand[NUM];  // 保存输入命令的数组
char* myargv[OPT_NUM];  // 保存选项的数组int  lastCode = 0;
int  lastSig = 0;int main()
{while(1){// 输出提示符printf("[用户名@主机名 当前路径]$");fflush(stdout);// 从键盘(stdin)获取指令输入  输入\nchar* s = fgets(lineCommand,sizeof(lineCommand)-1,stdin);//最后一个位置来保存极端场景下的\0if(s == NULL){perror("fgets");exit(1);}//消除命令行中最后的换行符lineCommand[strlen(lineCommand)-1] = '\0', // 将输入的字符串解析为说个字符串存放到myargv数组中,即字符串切割// "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->nmyargv[0] = strtok(lineCommand," ");int i = 1;if(myargv[0] != NULL && strcmp(myargv[0],"ls") == 0){myargv[1++] = (char*)"--color=auto";}// 如果没有子串了,strtok->NULL, myargv[end] = NULLwhile(myargv[i++] = strtok(NULL," "));// cd 改变父进程的路径// 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口// 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令if(myargv[0] != NULL && strcmp(myargv[0],"cd") == 0){if(myargv[1] != NULL){chdir(myargv[1]);// myargv[1]中保存着指定路径}continue;  // 下面的语句不需要在继续执行了,因为你cd的目的已经达到了}if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0){if(strcmp(myargv[1], "$?") == 0){printf("%d, %d\n", lastCode, lastSig);}else{printf("%s\n", myargv[1]);}continue;}// 创建子进程pid_t id = fork();if(id == -1){perror("fork");exit(1);}else if(id == 0){// 子进程进行进程程序替换execvp(myargv[0],myargv);exit(1);}else{int status = 0;pid_t ret = waitpid(id, &status,0);if(ret == -1){perror("waitpid");exit(1);}   lastCode = ((status >> 8) & 0xff);lastSig = (status & 0x7f);}}return 0;
}

相关文章:

【Linux】进程控制--进程创建/进程终止/进程等待/进程程序替换/简易shell实现

文章目录 一、进程创建1.fork函数2.fork函数返回值3.写时拷贝4.fork常规用法5.fork调用失败的原因 二、进程终止1.进程退出码2.进程退出场景3.进程常见退出方法 三、进程等待1.为什么要进行进程等待2.如何进行进程等待1.wait方法2.waitpid方法3.获取子进程status4.进程的阻塞等…...

用pip更新、安装python的包

查看pip的版本&#xff1a;python -m pip --version 例如&#xff0c;查看下pip的版本&#xff0c;在cmd下输入命令python -m pip --version&#xff0c;可以发现当前安装的pip的版本是23.2.1&#xff1a; 查看一个包的详情&#xff1a;python -m pip show 例如&#xff0c…...

spring boot 事件机制

目录 概述实践监听spring boot ready事件代码 源码初始化流程调用流程 结束 概述 spring boot 版本为 2.7.17 。 整体看一下spring及spring boot 相关事件。 根据下文所给的源码关键处&#xff0c;打上断点&#xff0c;可以进行快速调试。降低源码阅读难度。 实践 spring…...

分布式版本管理系统---->Git(Linux---centos(保姆式)讲解1)

文章目录: 1:什么是Git以及作用 2.Git的基本操作过程(创建git仓库,配置仓库的配置) 3.git的工作区&#xff0c;暂存区&#xff0c;版本库的关系 4.将文件添加到版本库&#xff1a;git add 与git commit -m命令 5.git log查看日志的引入 6.查看.git文件中的内容 7.修改文件内容查…...

B树你需要了解一下

介绍B树的度数主要特点应用场景时间复杂度代码示例拓展 介绍 B树&#xff08;B-tree&#xff09;是一种自平衡的树&#xff0c;能够保持数据有序&#xff0c;常被用于数据库和文件系统的实现。 B树可以看作是一般化的二叉查找树&#xff0c;它允许拥有多于2个子节点。与自平衡…...

MFC设置状态栏文本导致崩溃的原因

文章目录 问题和原因解决办法1.消息机制2.定时器问题和原因 本人在类A使用多线程执行操作并且调用了类B的设置状态栏文本的函数,导致崩溃 类A void A::distribute_n_start_msg(){((B*)m_parent)->received_msg_n_start...

配置typroa上传图片到gitee

一、gitee相关配置 到gitee官网创建一个新的仓库并获取其token gitee配置时候一定要新建仓库之后初始化好仓库 比如&#xff1a;创建出README.md文档 出现master这个显示界面&#xff0c;刚开始未初始化的时候是会报错的 二、typora相关配置 在typora这个位置下载插件 在p…...

java并发-线程生命周期

文章目录 前言状态图状态变化说明补充说明 前言 线程的生命周期指的是线程从创建出来到最终消亡的整个过程&#xff0c;以及过程中的状态变化。 状态图 以下图用mermaid语法绘制&#xff1a; #mermaid-svg-32vKT6KmFdlYvCnr {font-family:"trebuchet ms",verdana,…...

Javaweb之Vue路由的详细解析

5 Vue路由 5.1 路由介绍 将资代码/vue-project(路由)/vue-project/src/views/tlias/DeptView.vue拷贝到我们当前EmpView.vue同级&#xff0c;其结构如下&#xff1a; 此时我们希望基于4.4案例中的功能&#xff0c;实现点击侧边栏的部门管理&#xff0c;显示部门管理的信息&am…...

力扣:196. 删除重复的电子邮箱(Python3)

题目&#xff1a; 表: Person ---------------------- | Column Name | Type | ---------------------- | id | int | | email | varchar | ---------------------- id 是该表的主键列(具有唯一值的列)。 该表的每一行包含一封电子邮件。电子邮件将不包含…...

Ruby和HTTParty库下载代码示例

ruby require httparty require nokogiri # 设置服务器 proxy_host "" proxy_port "" # 定义URL url "" # 创建HTTParty对象&#xff0c;并设置服务器 httparty HTTParty.new( :proxy > "#{proxy_host}:#{proxy_port}" ) …...

Unity 使用Horizontal Layout Group和Toggle制作多个水平开关按钮实现自动排列和单个点击放大后的自动排列。

Unity的布局组件Horizontal Layout Group是很好用的&#xff0c;当然也包括其它布局组件也一样好用。 比如要实现多按钮开关自动水平排列&#xff0c;那么就可以使用它了。 首先我们为按钮创建个父物体&#xff08;我这里使用了Scroll View中的Content作为父物体&#xff09;…...

Python实现FA萤火虫优化算法优化BP神经网络回归模型(BP神经网络回归算法)项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 萤火虫算法&#xff08;Fire-fly algorithm&#xff0c;FA&#xff09;由剑桥大学Yang于2009年提出 , …...

灯塔ARL-NPoC全面教程

灯塔ARL-NPoC全面教程 1.ARL-NPoC2.安装3.参数解析4.ARL-NPoC编写指南标准POC模板`__init()__`verifyexploit_cmd5.将指纹同步到远程Web服务器1.ARL-NPoC 最新版的arl增加了poc编写与探测的功能,ARL-NPoC是一个集漏洞验证和任务运行的一个框架 2.安装 ARL-NPoC下载地址 下载…...

λ表达式、智能指针

lambda 表达式 1、C11标准支持&#xff0c;实现匿名函数的功能&#xff1b; 2、通常用于实现轻量级的函数 格式 mutable->返回值{函数体}; // 返回值即使是 void 也必须得写 [] 内&#xff0c;可以填外部数据&#xff1b; () 内&#xff0c;可以带有参数列表。 lambda 表达…...

PHP基础知识和操作

PHP在线运行 https://c.runoob.com/compile/1/ https://www.sotool.net/php80 将驼峰字符串转化为蛇形字符串 <?phpfunction CamelToSnake($camelValue) {$initValue preg_replace(/\s/u, , $camelValue);$snakeValue strtolower(preg_replace(/(.)(?[A-Z])/u, &quo…...

系列十三、SpringBoot的自动配置原理分析

一、概述 我们知道Java发展到现在功能十分的强大&#xff0c;生态异常的丰富&#xff0c;这里面离开不了Spring及其家族产品的支持&#xff0c;而作为Spring生态的明星产品Spring Boot可以说像王者一般的存在&#xff0c;那么的耀眼&#xff0c;那么的光彩夺目&#xff01;那么…...

soapui报错: CXF directory must be set in global preferences

文章目录 下载官网下载网盘下载 配置 soapui生成代码时报错 CXF directory must be set in global preferences 下载 需要下载apache-cxf。 官网下载 官网地址&#xff1a; https://www.apache.org/dyn/closer.lua/cxf/3.5.4/apache-cxf-3.5.4.zip 点如下地址即可。 The obj…...

Netty02-基础概念

什么是netty ​ Netty是一个基于Java NIO的异步事件驱动网络应用程序框架。它提供了简单易用的API&#xff0c;用于快速开发可维护的高性能网络应用程序。Netty的设计目标是提供一种高度可扩展的、高性能的网络应用程序框架&#xff0c;使得开发人员能够轻松地构建各种类型的网…...

计算机毕业设计 基于SpringBoot的敬老院管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…...

精调llama模型

github地址&#xff1a;https://github.com/facebookresearch/llama-recipes github:https://github.com/facebookresearch/llama import torch from transformers import LlamaForCausalLM, LlamaTokenizer#model_id"./models_hf/7B" # 可以从huggingface上面下载模…...

【C语言】深入理解C语言中的数学运算和类型转换

文章目录 引言取负运算的奥秘源码探索分析与解读 浮点数运算的精细差异源码分析精度损失与隐式类型转换 精度和除零运算探究float类型和double类型的精度各是多少&#xff08;即十进制有效位的位数&#xff09;&#xff1f;在你的机器上&#xff0c;“负数开方”是如何处理的&a…...

基于javaweb的宠物服务商城系统设计与开发

摘 要 最近几年以来&#xff0c;宠物在人们的日常生活中所占的地位越来越重要了&#xff0c;它们不仅仅是我们的朋友&#xff0c;也成为了我们家庭中的一份子。21世纪&#xff0c;信息技术飞速发展&#xff0c;计算机行业日新月异&#xff0c;极大地带动了信息的流动&#xff…...

LeetCode-470. 用 Rand7() 实现 Rand10()【数学 拒绝采样 概率与统计 随机化】

LeetCode-470. 用 Rand7 实现 Rand10【数学 拒绝采样 概率与统计 随机化】 题目描述&#xff1a;解题思路一&#xff1a;首先说一个结论就是(rand_X() - 1) Y rand_Y() > [1,X*Y]&#xff0c;即可以等概率的生成[1, X * Y]范围的随机数&#xff0c;其实就像军训的时候报数…...

通达信指标公式19:龙虎榜股票池——主力控盘度的计算方法

0.小红牛本指标&#xff0c;选股的思路说明&#xff1a;控盘度&#xff0c;又称主力控盘&#xff0c;是指主力控制了某只股票的大部分流通股&#xff0c;从而控制了股票的价格。主力控盘的目的通常是为了获取更多的收益&#xff0c;通过控制股票价格来实现其策略。所以首要分析…...

手搓图片滑动验证码_JavaScript进阶

手搓图片滑动验证码 背景代码效果图展示网站 背景 在做前端项目开发的时候&#xff0c;少不了登录注册部分&#xff0c;既然有登录注册就少不了机器人验证&#xff0c;验证的方法有很多种&#xff0c;比如短信验证码、邮箱验证码、图片滑动、图片验证码等。 由于鄙人在开发中…...

Linux服务器超级实用的脚本

1.使用INOTIFY+RSYNC自动实时同步数据 代码执行: bash inotify_rsyncs.sh :cat inotify_rsyncs.sh 脚本内容如下: #!bing/bash # Author: reyn #检测/data路径下的文件变化,排除Temp目录 INOTIFY_CMD="inotifywait -mrq -e modify,create,move,delete /data/ --exc…...

IntelliJ IDEA安装使用教程#intellij idea

做为基础开发软件&#xff0c;idea、pycharm、phpstorm是高级企业级开发中常用的图形化工具。 安装非常简单&#xff1a;去官网下载即可&#xff0c;有社区版本、有企业版本&#xff1a; IntelliJ IDEA – 领先的 Java 和 Kotlin IDE 因版权问题&#xff1a;这里不方面多讲。…...

【组合数学】容斥鸽巢原理

目录 1. 容斥原理容斥原理三种形式 2. 容斥原理应用有限重复数的多重集合的 r 组合数错排问题 3. 鸽巢原理4. Ramsey 定理 1. 容斥原理 容斥原理提供了一种通过计算每个单独集合的大小&#xff0c;然后修正重复计数的方法&#xff0c;从而得到多个集合并集大小的计算方法。它通…...

视频后期特效处理软件 Motion 5 mac中文版

Motion mac是一款运动图形和视频合成软件&#xff0c;适用于Mac OS平台。 Motion mac软件特点 - 精美的效果&#xff1a;Motion提供了多种高质量的运动图形和视频效果&#xff0c;例如3D效果、烟雾效果、粒子效果等&#xff0c;方便用户制作出丰富多彩的视频和动画。 - 高效的工…...

wordpress快速入门/今日新闻 最新消息 大事

​扩展阅读&#xff1a;营销中国站长“大米”&#xff0c;原黑鹰安全网站长&#xff0c;拥有十多年互联网从业经验。熟知网络营销、电子商务、各种网络赚钱方法。精通百度竞价&#xff0c;曾带领团队通过百度竞价创造单月上百万的业绩。还是多家中小型企业的网络营销顾问。 近…...

做刀模网站/企业线上培训平台有哪些

原文链接&#xff1a;工程师男友如何反窃听&#xff1f;趣聊密码学入门科普 阿里妹导读&#xff1a;谁都不想在通信过程中被别人“窃取”小秘密。本文借助一对情侣与八卦女、猥琐男的斗智故事&#xff0c;为大家讲述科普密码学基础知识。既有料又有趣&#xff0c;深入浅出&…...

个人简介网站html代码/一份完整的品牌策划方案

由于某些课程实验的要求&#xff0c;需要通过xposed框架对某应用进行hook操作&#xff0c;笔者选用了开源且免费的xposed框架进行实现。虽然网上存在一些利用xposed实现特定功能的文章资源&#xff0c;但大多均将xposed模块的构建作为一个小节内容一笔带过&#xff0c;而且介绍…...

flash中国官网/桔子seo工具

今天在做jasper report生成pdf文档的时候&#xff0c;需要引入亚洲字体jar包。maven仓库是有这个jar包&#xff0c;但是在项目pom文件始终不能下载。无奈只有将jar包安装到maven本地仓库。 1 将jar包放到电脑的某个位置&#xff0c;如&#xff1a;C:\E\Received\FireFox\itext-…...

平面设计零基础难学吗/网站内部链接优化方法

很久之前就对jQuery animate的实现非常感兴趣&#xff0c;不过前段时间很忙&#xff0c;直到前几天端午假期才有时间去研究。 jQuery.animate的每种动画过渡效果都是通过easing函数实现的。jQuery1.4.2中就预置了两个这样的函数&#xff1a; easing: {linear: function( p, n, …...

广西南宁房产网站建设/地推app接任务平台

0x01 漏洞利用知识点 1.代码执行 2.变量覆盖 3.文件包含 0x02 漏洞分析 首先在/fenlei1.0/do/jf.php文件中存在代码执行片段 $query2 $db->query("SELECT * FROM {$pre}jfabout WHERE fid$rs[fid] ORDER BY list");while($rs2 $db->fetch_array($query2)){ev…...