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

【Linux详解】——进程控制(创建、终止、等待、替换)

📖 前言:本期介绍进程控制(创建、终止、等待、替换)。


目录

  • 🕒 1. 进程创建
    • 🕘 1.1 fork函数初识
    • 🕘 1.2 fork的返回值问题
    • 🕘 1.3 写时拷贝
    • 🕘 1.4 创建多个进程
  • 🕒 2. 进程终止
    • 🕘 2.1 进程退出码
    • 🕘 2.2 进程如何退出
  • 🕒 3. 进程等待
    • 🕘 3.1 进程等待的必要性
    • 🕘 3.2 进程等待的方法
      • 🕤 3.2.1 回收子进程资源wait
      • 🕤 3.2.2 获取子进程的退出信息waitpid
      • 🕤 3.2.3 获取子进程status
    • 🕘 3.3 再谈进程退出
    • 🕘 3.4 进程的阻塞和非阻塞等待
      • 🕤 3.4.1 阻塞状态VS非阻塞状态
  • 🕒 4. 进程替换
    • 🕘 4.1 演示
    • 🕘 4.2 原理
      • 🕤 4.2.1 多进程问题
    • 🕘 4.3 替换函数
      • 🕤 4.3.1 execlp
      • 🕤 4.3.2 execv
      • 🕤 4.3.3 execvp
      • 🕤 4.3.4 execle
    • 🕘 4.4 调用自己创建的程序
    • 🕘 4.5 应用场景:模拟shell命令行解释器
      • 🕤 4.5.1 当前路径的概念
      • 🕤 4.5.2 改变当前路径:chdir函数

🕒 1. 进程创建

🕘 1.1 fork函数初识

前面我们提到过,fork函数就是从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

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

那么在调用fork函数之前只有一个进程,当进程调用fork时,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程(内核数据结构:PCB地址空间+页表,构建对应的映射关系)
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中(哈希表存储)
  • fork返回,开始调度器调度

🕘 1.2 fork的返回值问题

  1. 如何理解fork函数有两个返回值问题?

在这里插入图片描述
对于fork函数,当调用时,fork函数内部会有两个执行流,对应父进程和子进程,当fork函数内部代码执行完毕后,子进程也就被创建好了并有可能在OS的运行队列中准备被调度了,父进程和子进程各自执行return,这样在main()函数中调用fork函数时,从fork返回的两个执行流就会分别执行main()调用fork之后的代码,因此我们之前所看到的两个结果就是父子进程对应的执行流所造成的。

  1. 如何理解fork返回之后,给父进程返回子进程pid,给子进程返回0?

根据实际生活我们知道父亲与孩子的关系是1:n,因此孩子找父亲具有唯一性。而由于子进程多,父进程想具体调用某一个子进程时就需要这个子进程得有一个名字才能调用这个子进程,因此给父进程返回对应子进程的pid。

  1. 如何理解同一个id值,怎么会保存两个不同的值,让if else if同时执行?

对于pid_t id = fork(),我们知道返回的本质就是写入,所以谁先返回,谁就先写入对应的id,由于进程具有独立性,因此进程就会进行写时拷贝,因此同一个id,地址是一样的,但内容却不同。

🕘 1.3 写时拷贝

在上一篇进程地址空间中,我们提到过什么是写时拷贝,这里总结一下。

通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。(虚拟内存就是进程地址空间)
在这里插入图片描述
即当我们不修改数据时,父子进程的虚拟内存所对应的物理内存都是同一块物理地址(内存),当子进程的数据被修改,那么就会将子进程修改所对应数据的物理内存出进行写时拷贝,在物理内存中拷贝一份放在物理内存的另一块空间,将子进程虚拟内存与这个新的地址通过页表进行关联。

🕘 1.4 创建多个进程

代码如下:

#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);}else{// partent;}cnt++;}return 0;
}

由于开的进程过多,会导致整个OS崩掉,只需要重启服务器就可以解决了。

🕒 2. 进程终止

🕘 2.1 进程退出码

我们在C/C++中,在代码最后都会写上return 0;,对于这个返回值我们称它为进程退出码。对于正确的进程一般都以0作为进程退出码,而非0就作为错误的进程的退出码,因此不同的错误对应的退出码也是不同的。

退出码的意义:0:success, !0:表示失败。!0具体是多少,即表示不同的错误。

#include <stdio.h>
int addToTarget(int from, int to)
{int sum = 0;for(int i = from; i < to; i++){sum += i;}return sum;
}
int main()
{int num = addToTarget(1, 100);if(num == 5050)return 0;elsereturn 1;// 进程退出时对应的退出码  // 标定进程执行的结果是否正确
}

我们可以通过echo $?来查看进程退出码

[hins@VM-12-13-centos exit]$ ./mytest
[hins@VM-12-13-centos exit]$ echo $?	# 记录最近一个进程在命令行中执行完毕时对应的退出码
1
[hins@VM-12-13-centos exit]$ echo $?	
0							# 这里为什么是0呢?答案是echo也是一个进程

之前我们C语言学过strerror(n),n为自然数,即n的不同的值就代表着不同的错误。需要头文件#include<string.h>。我们可以用下面一段代码来观察错误含义有哪些。

for(int i=0; i<200; i++)
{printf("%d: %s\n", i, strerror(i));
}
0:Success
1:0peration not permitted
2:No such file or directory
3:No such process
4:Interrupted system call
5:Input/output error
6:No such device or address
......

综上我们可以总结一下:

  • 代码运行结束,结果正确 ——— return 0;
  • 代码运行结束,结果不正确———return !0; (退出码这个时候起效果。确定对应的错误)
  • 代码运行异常,退出码无意义。

🕘 2.2 进程如何退出

  1. main函数return返回

这也是我们经常用的方式

  1. 任意地方调用 exit(code)退出

code为退出码,下面就演示一下:

#include <stdio.h>
#include <stdlib.h>
int main()
{printf("Hello World!\n");  exit(12); 
}
[hins@VM-12-13-centos exit]$ ./mytest
Hello World!
[hins@VM-12-13-centos exit]$ echo $?
12

由此,当我们想知道进程如何结束的时候,可以直接观察退出码。

在函数内部exit时,进程也会直接结束,函数也不会有返回值,下面就来看看这个例子:

#include <stdio.h>
#include <stdlib.h>
int addToTarget(int from, int to)
{int sum = 0;for(int i = from; i < to; i++){sum += i;}exit(15); // return sum;
}
int main()
{printf("Hello World!\n");  int ret = addToTarget(1, 100);printf("%d\n", ret);  
}
[hins@VM-12-13-centos exit]$ ./mytest
Hello World!
[hins@VM-12-13-centos exit]$ echo $?
15

到exit语句就会将进程结束,后面的代码也就不会再去执行了。

  1. _exit()退出
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int addToTarget(int from, int to)
{int sum = 0;for(int i = from; i < to; i++){sum += i;}//exit(15); 	// 库函数_exit(31);		// 系统调用// return sum;
}
int main()
{printf("Hello World!\n");  int ret = addToTarget(1, 100);printf("%d\n", ret);  
}
[hins@VM-12-13-centos exit]$ ./mytest
Hello World!
[hins@VM-12-13-centos exit]$ echo $?
31

我们发现它和exit()是一样的功能。事实上,_exit()是OS调用的函数,而exit()是库函数,库函数是OS之上的函数,调用exit实际上就是exit内部调用_exit,但二者之间也会有区别,我们将换行符去掉,来演示一下:exit

int main()
{printf("Hello World!");  sleep(2);exit(6);		// 测试1// _exit(6);	// 测试2
}

请添加图片描述
可以看出如果是exit,进程结束后,会刷新缓冲区,打印的结果暂停2秒也会显示出来,_exit就不会。

因此可以得出总结:

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

在这里插入图片描述
在这里插入图片描述

因此用户级的缓冲区一定在系统调用之上,具体会在基础IO的时候说明。

🕒 3. 进程等待

🕘 3.1 进程等待的必要性

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

🕘 3.2 进程等待的方法

🕤 3.2.1 回收子进程资源wait

我们可以通过man 2 wait打开手册了解该函数:
在这里插入图片描述

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为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 == 0){// 子进程int cnt = 5;while (cnt){printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);}exit(0); // 进程退出}// 父进程sleep(10);	// 这样子进程会由于没有父进程回收会出现5秒Z状态pid_t ret = wait(NULL);if (id > 0){printf("wait success:%d\n", ret);}sleep(5);
}

我们调出进程查看器观察:while :; do ps ajx | head -1 && ps ajx | grep mytest | grep -v grep; sleep 1; done

请添加图片描述

🕤 3.2.2 获取子进程的退出信息waitpid

在这里插入图片描述

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。

同样举个例子:

#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("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);}exit(10); // 进程退出}// 父进程int status = 0; // 不是被整体使用的,有自己的位图结构pid_t ret = waitpid(id, &status, 0);if(id > 0){printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status>>8)&0xFF);}sleep(5);
}
[hins@VM-12-13-centos exit]$ ./mytest
我是子进程: 24005, 父进程: 24004, cnt: 5
我是子进程: 24005, 父进程: 24004, cnt: 4
我是子进程: 24005, 父进程: 24004, cnt: 3
我是子进程: 24005, 父进程: 24004, cnt: 2
我是子进程: 24005, 父进程: 24004, cnt: 1
wait success: 24005, sig number: 0, child exit code: 10

🕤 3.2.3 获取子进程status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

而上面所说的实际上就是:对于这个拿到子进程的退出结果,实际上并不能直接反应出我们想要的结果,其结果是一个复合类型,我们需要将其进行拆分:
在这里插入图片描述

对于32个bit位在这里只有尾部16个bit位是有意义的,因此我们将这些拿出来,即0~7位返回0代表正常的终止信号(即没有出问题),8~15次低8位代表子进程对应的退出码。

我们往子进程加个错误,模拟代码没跑完的情况。

...
while (cnt)
{printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);int *p = NULL;*p = 100;		// 错误
}
...
[hins@VM-12-13-centos exit]$ ./mytest
我是子进程: 25101, 父进程: 25100, cnt: 5
wait success: 25101, sig number: 11, child exit code: 0

可以看到sig number显示为11,代表有问题,通过kill -l可以发现问题是段错误。

在这里插入图片描述
阅读struct task_struct的源码,我们发现对于进程退出码和终止信号都在这个PCB中。即我们可以总结成一张图:

在这里插入图片描述
上述的过程我们也再总结一下:

  1. 让OS释放子进程的僵尸状态
  2. 获取子进程的退出结果(如果子进程不结束,父进程就会一直处于阻塞等待,等待子进程退出)

对于WIFEXITED(status)WEXITSTATUS(status)的使用,接下来举个父子进程正常运行时杀掉子进程的例子:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>int main()
{pid_t id = fork();assert(id != -1);if (id == 0){//childint cnt = 50;while (cnt){printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);}exit(11);}// parentint status = 0;int ret = waitpid(id, &status, 0);if (ret > 0){// 是否正常退出if (WIFEXITED(status)){// 判断子进程运行结果是否OKprintf("exit code: %d\n", WEXITSTATUS(status));}else{printf("child exit not normal!\n");}}return 0;
}

调出进程查看器观察:ps ajx | head -1 && ps ajx | grep mychild
在这里插入图片描述

🕘 3.3 再谈进程退出

  • 进程退出会变成僵尸,会把自己的退出结果写入到自己的task_struct中
  • wait/waitpid 是一个系统调用,即以OS的身份进行,因此OS也有能力去读取子进程的status。

即前两条都意味着子进程的退出信号和退出结果都保留在子进程的PCB中。

🕘 3.4 进程的阻塞和非阻塞等待

首先以一个例子来解释阻塞和非阻塞:

到饭点了,你和舍友相约一起去干饭,但是舍友王者还没结束,于是你决定看着舍友打完再去干饭。第二天饭点时,你又遇到了舍友王者还没结束的情况,于是决定自己刷会儿视频,然后时不时看看舍友水晶爆了没,等到那声victory出现后,你们便一起去干饭了。

对于上述这个例子,看着舍友打完这个过程就是一种阻塞状态,因为你一直在检测舍友状态(即是否打完王者),而你也没有下一步的行动。第二天同一情况时,你选择刷会儿视频,然后时不时检测舍友状态,这种时不时检测的行为就是一种非阻塞状态,而这多次非阻塞就是一个轮询的过程。因此看舍友状态就相当于系统调用wait/waitpid,你就相当于父进程,舍友就相当于子进程。

对于阻塞等待,我们上面已经演示过,那么下面就直接上非阻塞状态的过程:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>int main()
{pid_t id = fork();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(3);}exit(11);}// 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......\n");}else if (ret > 0){// waitpid调用成功&&子进程退出了printf("wait success, exit code: %d, sign: %d\n", (status >> 8) & 0xFF, status & 0x7F );break;}else{// waitpid调用失败printf("waitpid call failed\n");break;}sleep(1);}return 0;
}

上述代码的意思是:子进程在执行期间,父进程则会一直等待并通过while的方式去轮询非阻塞状态,直到子进程退出。

[hins@VM-12-13-centos child]$ ./mychild
wait done, but child is running......
child running, pid: 22017, ppid: 22016, cnt: 10
wait done, but child is running......
wait done, but child is running......
child running, pid: 22017, ppid: 22016, cnt: 9
wait done, but child is running......
wait done, but child is running......
wait done, but child is running......
child running, pid: 22017, ppid: 22016, cnt: 8
wait done, but child is running......
wait done, but child is running......
wait done, but child is running......
......
child running, pid: 22017, ppid: 22016, cnt: 1
wait done, but child is running......
wait done, but child is running......
wait done, but child is running......
wait success, exit code: 11, sign: 0

如果子进程出异常了,那么父进程也能够抓到,为了演示这种情况我们在子进程中增加一个野指针的错误:

...
while (cnt)
{printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(3);int *p = NULL;*p = 100;		// 野指针问题
}
...
[hins@VM-12-13-centos child]$ ./mychild
wait done, but child is running......
child running, pid: 23075, ppid: 23074, cnt: 10
wait done, but child is running......
wait done, but child is running......
wait success, exit code: 0, sign: 11

此时子进程的退出码为0,而终止信号是11号,对于异常的进程退出,他的退出码是没有意义的,所以我们返回为0的退出码不看。

那什么时候会等待失败呢?id错误的时候会等待失败。

......while (1){pid_t ret = waitpid(id+1, &status, WNOHANG);	// id+1,错误的idif (ret == 0)
......
[hins@VM-12-13-centos child]$ ./mychild
waitpid call failed		# 等待失败
child running, pid: 23924, ppid: 23923, cnt: 10
[hins@VM-12-13-centos child]$ child running, pid: 23924, ppid: 1, cnt: 9
child running, pid: 23924, ppid: 1, cnt: 8
[hins@VM-12-13-centos child]$ child running, pid: 23924, ppid: 1, cnt: 7
child running, pid: 23924, ppid: 1, cnt: 6
^C		# 此时子进程被领养,CTRL+C无效
[hins@VM-12-13-centos child]$ child running, pid: 23924, ppid: 1, cnt: 5
child running, pid: 23924, ppid: 1, cnt: 4
child running, pid: 23924, ppid: 1, cnt: 3
# 可以kill -9 23924 杀掉子进程
......

🕤 3.4.1 阻塞状态VS非阻塞状态

非阻塞状态的好处:不会占用父进程的所有精力,可以在轮询期间,干干别的。那么就来用代码演示一下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <assert.h>
#define NUM 10
typedef 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();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] != 0; i++){handlerTask[i]();	// 采用回调的方式,执行我们想让父进程在空闲的时候做的事情}}else if (ret > 0){// waitpid调用成功&&子进程退出了printf("wait success, exit code: %d, sign: %d\n", (status >> 8) & 0xFF, status & 0x7F );break;}else{// waitpid调用失败printf("waitpid call failed\n");break;}sleep(1);}return 0;
}
[hins@VM-12-13-centos child]$ ./mychild
wait done, but child is running......parent running other things
handler task1
handler task2
handler task3
child running, pid: 27116, ppid: 27115, cnt: 10
wait done, but child is running......parent running other things
handler task1
handler task2
handler task3
child running, pid: 27116, ppid: 27115, cnt: 9......
wait done, but child is running......parent running other things
handler task1
handler task2
handler task3
child running, pid: 27116, ppid: 27115, cnt: 1
wait done, but child is running......parent running other things
handler task1
handler task2
handler task3
wait success, exit code: 10, sign: 0

也许有同学认为非阻塞是最好的,其实不然,这两个状态是并行存在的,并没有好坏之分。

接下来小结一下:

  • 进程等待是什么?

答:通过系统调用,让父进程等待子进程的一种方式。

  • 进程为什么要等待?

答:释放子进程僵尸,获取子进程状态。(退出码,退出信号)

  • 进程怎么等待?

答:通过wait/waitpid通过指定方式阻塞或者非阻塞的方式进行等待。

🕒 4. 进程替换

创建子进程的目的:

  1. 想让子进程执行父进程代码的一部分(执行父进程对应磁盘代码中的一部分)
  2. 想让子进程执行一个全新的程序(让子进程想办法加载磁盘是指定的程序,执行新程序的代码和数据,这就是进程的程序替换)

🕘 4.1 演示

在这一小节中,包含6种函数,为了演示,就在这里拿出一个函数看看进程程序替换究竟是什么样子。

int execl(const char *path, const char *arg, ...);//将指定的程序加载到内存中,让指定进程进行执行

对于一个程序加载到内存去执行,首先是找到这个程序,然后通过不同的选项去以不同的方式去执行,这与环境变量是一样的。因此对于此execl函数来讲,第一个参数path就代表找到程序对应的路径,第二个就代表选项,选哪种方式运行程序的选项;而后面的...,我们为他引入一个新的名词:可变参数列表。顾名思义我们在C语言中的scanf以及printf类的函数,无论传入多少个参数都没有限制,实际上就是可变参数列表的作用,因此,excel里的可变参数列表的作用就是让我们能在传入选项参数时能够传入任意数量的选项。(如 cmd 选项1,选项2……)

知道了这个函数功能之后,开始操作:

#include <stdio.h>
#include <unistd.h>
int main()
{// .c->exe->load->process->运行->执行我们所写的代码printf("process is running...\n");	// load->exeexecl("/usr/bin/ls"/*要执行哪个程序*/,"ls","--color=auto"/*颜色方案*/, "-a", "-l", NULL/*你想怎么执行*/);	// all exec* end of NULLprintf("process is running...\n" );return 0;
}

在这里插入图片描述
可以看到这个函数实现的效果与原来的指令无异,这就叫进程的程序替换。

但是我们发现第一个printf打印出来了,但是execl后面的printf却没有打印出来,这是为什么呢?通过下面理解:

🕘 4.2 原理

程序替换的本质,就是将指定程序的代码和数据加载到指定的位置(覆盖自己的代码和数据),进程替换的时候,并没有创建新的进程。

在这里插入图片描述
当我们执行代码时,就会创建进程地址空间与物理内存磁盘之间形成映射关系,当执行上面的代码时就是这样,执行第一个printf会照常打印,到了execl函数时,就会发生进程的程序替换,也就是说,我们所编写的代码会被我们调用的execl对应磁盘内部的代码覆盖,即将指定程序的代码和数据覆盖自己的代码和数据,执行这个新的代码和数据,所以我们明白了为什么execl后面的printf没有执行。

我们知道,只要是一个函数调用就有可能失败,即没有替换成功,而对于这exec系列的函数,失败了返回-1,则程序不被替换,因此execl下面的代码也会继续执行。成功替换则没有返回值。

🕤 4.2.1 多进程问题

这次我们通过fork创建子进程,并在子进程执行对应的execl函数:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>int main()
{printf("process is running...\n");pid_t id = fork();assert(id != -1);if (id == 0){// child// 这里的替换,会影响父进程吗?// 类比:命令行怎么写,这里就怎么传sleep(1);execl("/usr/bin/ls", "ls", "-a", "-l", "--color=auto", NULL);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);return 0;
}

如果我们随便打一个不存在的位置或者程序,那么code的值就会变成-1。那这个时候,子进程调用的execl会影响父进程吗?答案当然是否定的,进位进程具有独立性,下面就来理解一下具体是什么原因:

在这里插入图片描述
当只存在一个父进程时,就会创建出上面这样的映射关系,当fork函数开始执行,子进程生成,就会创建出子进程的PCB,以及对应的虚拟内存、页表,与父进程共享对应的物理内存。而当子进程调用execl时,由于子进程发生改变,本着进程直之间具有独立性的原则,子进程就会发生写时拷贝,将共享的数据段和代码段在物理内存的另一个位置进行写时拷贝,并与新的位置形成映射,这样便不会影响到父进程。此外我们也可以看出,数据和代码都可以发生写时拷贝。

总结: 虚拟地址空间+页表保证进程独立性,一旦有执行流想替换代码或者数据,就会发生写时拷贝

🕘 4.3 替换函数

除了execl,还有其他类似的接口,六种以exec开头的函数,统称exec函数。

#include <unistd.h>`
int execl(const char *path, const char *arg, ...);// l(list) : 表示参数采用列表。
int execlp(const char *file, const char *arg, ...);// p(path) : 有p自动搜索环境变量PATH,只要传入的名字不需要具体路径。
int execle(const char *path, const char *arg, ...,char *const envp[]);// e(env) : 表示自己维护环境变量。
int execv(const char *path, char *const argv[]);// v(vector) : 参数用数组,统一传递,而不用进行使用可变参数方案。
int execvp(const char *file, char *const argv[]);// vp就是v和p的结合。

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

函数的具体使用如下:

🕤 4.3.1 execlp

execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);	// 只写一个ls也可以

上面的两个ls是不重复的,第一个ls代表着要执行谁,第二个ls代表着要怎么执行。

🕤 4.3.2 execv

char *const argv_[] = {"ls","-a","-l","--color=auto",NULL
};
execv("/usr/bin/ls", argv_);

🕤 4.3.3 execvp

v,p就是组合在一起

char *const argv_[] = {"ls","-a","-l","--color=auto",NULL
};
execvp("ls", argv_);

🕤 4.3.4 execle

注:先阅读4.4小节完再看本小节。

// 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("I'm another C program.\n");printf("I'm another C program.\n");printf("I'm another C program.\n");                                             return 0;
}            
// myexec.c
char *const envp_[] = {(char*)"MYENV=11112222233334444",NULL
};
execle("./mybin", "mybin", NULL, envp_); // 自定义环境变量
[hins@VM-12-13-centos exec]$ ./myexec
process is running...
total 44
drwxrwxr-x  2 hins hins 4096 Feb 15 19:23 .
drwxrwxr-x 11 hins hins 4096 Feb 15 12:33 ..
-rw-rw-r--  1 hins hins  146 Feb 15 17:46 Makefile
-rwxrwxr-x  1 hins hins 8464 Feb 15 19:23 mybin
-rw-rw-r--  1 hins hins  347 Feb 15 19:23 mybin.c
-rwxrwxr-x  1 hins hins 8824 Feb 15 19:23 myexec
-rw-rw-r--  1 hins hins 1147 Feb 15 19:22 myexec.c
wait success: exit code: 0, sig: 0
PATH:(null)
PWD:(null)
MYENV:11112222233334444
I'm another C program.
I'm another C program.
I'm another C program.

发现这样使用之后,系统内部的环境变量使用不了,只能使用自定义的。这是因为我们的函数的最后一个参数的原因,即传入的环境变量,没有传入就不会使用,因此如果我们这么改:

extern char **environ;
execle("./mybin", "mybin", NULL, environ); // 系统环境变量

就会反过来:我们自定义的环境变量就不会生效,只有系统的才会生效。

但是我们想让两者同时生效,这就引入了一个前几章提到的函数:putenv

extern char **environ;
putenv((char*)"MYENV=4443332211"); //将指定环境变量导入到系统中 environ指向的环境变量表
execle("./mybin", "mybin", NULL, environ); // 系统环境变量

忽略警告,结果如下:

[hins@VM-12-13-centos exec]$ ./myexec
process is running...
total 44
drwxrwxr-x  2 hins hins 4096 Feb 15 19:42 .
drwxrwxr-x 11 hins hins 4096 Feb 15 12:33 ..
-rw-rw-r--  1 hins hins  146 Feb 15 17:46 Makefile
-rwxrwxr-x  1 hins hins 8464 Feb 15 19:23 mybin
-rw-rw-r--  1 hins hins  347 Feb 15 19:23 mybin.c
-rwxrwxr-x  1 hins hins 8952 Feb 15 19:42 myexec
-rw-rw-r--  1 hins hins 1268 Feb 15 19:42 myexec.c
wait success: exit code: 0, sig: 0
PATH:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/hins/.local/bin:/home/hins/bin
PWD:/home/hins/testLinux/exec
MYENV:4443332211
I'm another C program.
I'm another C program.
I'm another C program.

这样就满足了我们的需求。

  • 问:对于execle函数和main函数,在进程调用的时候是谁先被调用呢?

在我们之前的代码中,main函数通常是这样的参数:(VS上没有是因为编译器在编译时自动生成)

int main(int argc, char* argv[], char* env[])

答:exec先被调用。exec系列的函数的功能是将我们的程序加载到内存中!我们知道一个程序要想运行必须加载到内存中让CPU去执行,而对于Linux来说,程序加载是通过exec系列的函数加载到内存中的,因此Linux中的exec系列函数也被称为加载器。因此我们毫无疑问,程序一定是先加载再执行main。

那main也作为函数,也需要被传参,exec系列的函数和main函数的参数关联如下:
在这里插入图片描述
事实上,他们的参数就是这种一一对应的映射关系!即main函数被exec调用!这是我们看不到的。

而对于exec系列中不带有env参数的那些函数,照样能够拿到默认的环境变量,原因是environ通过地址空间的方式让子进程拿到的。

对于虚拟地址空间,我们回忆一下,
在这里插入图片描述

对于这个命令行参数和环境变量,就是通过第三方变量environ这个虚拟地址,以这个地址作为起始就可以拿到所有的环境变量,如果需要的话,也可以通过这个虚拟地址传入到main函数参数里去使用。

以上就是程序替换的全部内容。对于execvpe,参数类型都是一样的,只不过是组合的形式出现而已,不再赘述。

此外,上面的exec类的函数,有了各种组合,观察规律发现,缺了一种组合:execve,那我们直接man execve查看对应的信息,发现其是单独出现在2号手册,而上面的那些函数都是在3号手册,最终得出一个结论:execve是唯一一个系统调用的接口,而上面的那些函数都是在execve基础上进行的封装!(封装是为了让我们有很多的选择性,提供给不同的替换场景)

现在就可以总结一下函数的特征:
函数名 参数格式 是否带路径 是否使用当前环境变量 execl 列表 不是 是 execlp 列表 是 是 execle 列表 不是 不是, 须自己组装环境变量 execv 数组 不是 是 execvp 数组 是 是 execve 数组 不是 不是, 须自己组装环境变量 \begin{array}{|l|l|l|l|} \hline \text { 函数名 } & \text { 参数格式 } & \text { 是否带路径 } & \text { 是否使用当前环境变量 } \\ \hline \text { execl } & \text { 列表 } & \text { 不是 } & \text { 是 } \\ \hline \text { execlp } & \text { 列表 } & \text { 是 } & \text { 是 } \\ \hline \text { execle } & \text { 列表 } & \text { 不是 } & \text { 不是, 须自己组装环境变量 } \\ \hline \text { execv } & \text { 数组 } & \text { 不是 } & \text { 是 } \\ \hline \text { execvp } & \text { 数组 } & \text { 是 } & \text { 是 } \\ \hline \text { execve } & \text { 数组 } & \text { 不是 } & \text { 不是, 须自己组装环境变量 } \\ \hline \end{array} 函数名  execl  execlp  execle  execv  execvp  execve  参数格式  列表  列表  列表  数组  数组  数组  是否带路径  不是    不是  不是    不是  是否使用当前环境变量      不是须自己组装环境变量      不是须自己组装环境变量 

在这里插入图片描述

(在使用中,忽略一些参数其实也是对的,但为了理解最好不要那样做!)

🕘 4.4 调用自己创建的程序

我们在同一个目录创建mybin.c,编写代码如下:

#include<stdio.h>
int main()
{printf("I'm another C program.\n");printf("I'm another C program.\n");printf("I'm another C program.\n");                                             return 0;
}            

我们需要用生成的myexec调用这个程序生成的mybin,因此在Makefile中也需要改成能够同时生成myexec和mybin的指令,对于Makefile文件,只会生成第一个程序,因此在这里这样改就可以同时生成:

.PHONY:all
all: mybin myexecmybin:mybin.cgcc -o $@ $^ -std=c99
myexec:myexec.cgcc -o $@ $^ -std=c99
.PHONY:clean
clean:rm -f myexec mybin

这样处理之后,再将原myexec.c中的内容少加改动:(注,mybin不是环境变量中的内容,因此不能用带p的函数)

execl("./mybin","mybin",NULL);   

处理完毕之后,结果如下:

[hins@VM-12-13-centos exec]$ make
gcc -o mybin mybin.c -std=c99
gcc -o myexec myexec.c -std=c99
[hins@VM-12-13-centos exec]$ ./myexec
process is running...
total 44
drwxrwxr-x  2 hins hins 4096 Feb 15 18:00 .
drwxrwxr-x 11 hins hins 4096 Feb 15 12:33 ..
-rw-rw-r--  1 hins hins  146 Feb 15 17:46 Makefile
-rwxrwxr-x  1 hins hins 8360 Feb 15 18:00 mybin
-rw-rw-r--  1 hins hins  168 Feb 15 17:52 mybin.c
-rwxrwxr-x  1 hins hins 8776 Feb 15 18:00 myexec
-rw-rw-r--  1 hins hins  969 Feb 15 18:00 myexec.c
wait success: exit code: 0, sig: 0
I'm another C program.
I'm another C program.
I'm another C program.

这样就通过myexe.c调用了自己创建的mybin程序了。

对于这种调用方式,是没有语言之间的隔阂的,即我们可以在多种编程语言之间互调(C++、Java、Python等)。

🕘 4.5 应用场景:模拟shell命令行解释器

我们将子进程的代码更改一下:

int main(int argc, char* argv[])
{printf("process is running...\n");pid_t id = fork();assert(id != -1);if (id == 0){sleep(1);execvp(argv[1], &argv[1]);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);return 0;
}

不传入argv[0]的原因是argv[0]代表自己的程序:myexec,这样的话就会出现死循环的情况,因为会一直调用,所以为了跳过,我们从第二个元素argv[1]的地址开始。

[hins@VM-12-13-centos exec]$ ./myexec ls -a -l --color=auto	# 依次代表argv[0]、argv[1]、argv[2]、argv[3]、argv[4]
process is running...
total 44
drwxrwxr-x  2 hins hins 4096 Feb 15 21:17 .
drwxrwxr-x 11 hins hins 4096 Feb 15 12:33 ..
-rw-rw-r--  1 hins hins  146 Feb 15 17:46 Makefile
-rwxrwxr-x  1 hins hins 8464 Feb 15 19:23 mybin
-rw-rw-r--  1 hins hins  347 Feb 15 19:23 mybin.c
-rwxrwxr-x  1 hins hins 8776 Feb 15 21:17 myexec
-rw-rw-r--  1 hins hins 1335 Feb 15 21:17 myexec.c
wait success: exit code: 0, sig: 0

那如果我们将第一个./myexec去掉,发现不就是相当于自己写了一个shell吗?因此下面我们来编写shell命令行解释器

新建目录myshell,然后写代码

# Makefile
myshell:myshell.cgcc -o $@ $^ -std=c99 #-DDEBUG
.PHONY:clean
clean:rm -f myshell
// myshell.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>#define NUM 1024
#define OPT_NUM 64char lineCommand[NUM];
char *myargv[OPT_NUM]; //指针数组int main()
{while(1){// 输出提示符printf("用户名@主机名 当前路径# ");fflush(stdout);// 获取用户输入, 输入的时候,输入\nchar *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);assert(s != NULL);(void)s;// 清除最后一个\n , abcd\nlineCommand[strlen(lineCommand)-1] = 0; // ?//printf("test : %s\n", lineCommand);// "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n// 字符串切割myargv[0] = strtok(lineCommand, " ");int i = 1;if (myargv[0] != NULL && strcmp(myargv[0], "ls") == 0){myargv[i++] = (char*)"--color=auto";}// 如果没有子串了,strtok->NULL, myargv[end] = NULLwhile(myargv[i++] = strtok(NULL, " "));// 测试是否成功, 条件编译
#ifdef DEBUGfor(int i = 0 ; myargv[i]; i++){printf("myargv[%d]: %s\n", i, myargv[i]);}
#endif// 内建命令 --> echo// 执行命令pid_t id = fork();assert(id != -1);if(id == 0){execvp(myargv[0], myargv);exit(1);}waitpid(id, NULL, 0);}
}

这样就可以很好的模拟出shell命令行解释器了,但还有一个问题:就是返回上一级路径时,对于我们这个代码是这样的情况:

[hins@VM-12-13-centos myshell]$ ./myshell
用户名@主机名 当前路径# pwd
/home/hins/testLinux/myshell
用户名@主机名 当前路径# cd ..
用户名@主机名 当前路径# pwd
/home/hins/testLinux/myshell

但是按照正常的命令行来说应该是变化的,因此下面就来尝试解决这个问题:

🕤 4.5.1 当前路径的概念

这里touch一个新的myproc.c来解释:

#include <stdio.h>
#include <unistd.h>int main()
{while (1){printf("I'm a process.%d\n", getpid());sleep(1);}return 0;
}

然后gcc mypro.c -o myproc
观察进程:ls /proc/进程id -al

在这里插入图片描述
当前进程的工作目录,就是当前路径。 因此,若是想实现路径的改变,就需要实现进程工作目录的改变,说到这里,大家也应该明白,这个当前进程的工作目录也是可以修改的。

🕤 4.5.2 改变当前路径:chdir函数

只需要在myproc.c加上一行chdir("/home/自定义路径");即可,这样当前进程的工作目录就被修改了

回到上面,为什么我们自己写的shell,cd .. 的时候路径没有变化呢?

在上面实现的shell模拟代码中,我们fork出了子进程,子进程有自己的工作目录,因此cd更改的是子进程的工作目录,子进程执行完毕,继续用的是父进程,就是我们的shell,因此在这个过程中父进程也就是shell的工作目录并没有发生变化。

接下来将编写的模拟shell进行修改:

......
while (myargv[i++] = strtok(NULL, " "));
// 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
if (myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
{if (myargv[1] != NULL) chdir(myargv[1]);continue;
}
......
[hins@VM-12-13-centos myshell]$ ./myshell
用户名@主机名 当前路径# pwd
/home/hins/testLinux/myshell
用户名@主机名 当前路径# cd ..
用户名@主机名 当前路径# pwd
/home/hins/testLinux

这样就补充了之前的不足。像cd这种不需要让我们的子进程来执行,而是让shell自己执行的命令,被称为内建/内置命令

还有一个问题:echo内建命令。对于echo我们知道,通过echo $? 能够获得最近一次进程的退出码和终止信号。

myshell最终代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>#define NUM 1024
#define OPT_NUM 64char lineCommand[NUM];
char* myargv[OPT_NUM]; //指针数组
int  lastCode = 0;
int  lastSig = 0;int main()
{while (1){// 输出提示符printf("用户名@主机名 当前路径# ");fflush(stdout);// 获取用户输入, 输入的时候,输入\nchar* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);assert(s != NULL);(void)s;// 清除最后一个\n , abcd\nlineCommand[strlen(lineCommand) - 1] = 0; // ?//printf("test : %s\n", lineCommand);// "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n// 字符串切割myargv[0] = strtok(lineCommand, " ");int i = 1;if (myargv[0] != NULL && strcmp(myargv[0], "ls") == 0){myargv[i++] = (char*)"--color=auto";}// 如果没有子串了,strtok->NULL, myargv[end] = NULLwhile (myargv[i++] = strtok(NULL, " "));// 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口if (myargv[0] != NULL && strcmp(myargv[0], "cd") == 0){if (myargv[1] != NULL) chdir(myargv[1]);continue;}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;}// 测试是否成功, 条件编译
#ifdef DEBUGfor (int i = 0; myargv[i]; i++){printf("myargv[%d]: %s\n", i, myargv[i]);}
#endif// 内建命令 --> echo// 执行命令pid_t id = fork();assert(id != -1);if (id == 0){execvp(myargv[0], myargv);exit(1);}int status = 0;pid_t ret = waitpid(id, &status, 0);assert(ret > 0);(void)ret;lastCode = ((status >> 8) & 0xFF);lastSig = (status & 0x7F);}
}
[hins@VM-12-13-centos myshell]$ ./myshell
用户名@主机名 当前路径# echo $?
0, 0
用户名@主机名 当前路径# echo 1234
1234
用户名@主机名 当前路径# ^C

OK,以上就是本期知识点“进程控制(创建、终止、等待、替换)”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟📌~
💫如果有错误❌,欢迎批评指正呀👀~让我们一起相互进步🚀
🎉如果觉得收获满满,可以点点赞👍支持一下哟~

❗ 转载请注明出处
作者:HinsCoder
博客链接:🔎 作者博客主页

相关文章:

【Linux详解】——进程控制(创建、终止、等待、替换)

&#x1f4d6; 前言&#xff1a;本期介绍进程控制&#xff08;创建、终止、等待、替换&#xff09;。 目录&#x1f552; 1. 进程创建&#x1f558; 1.1 fork函数初识&#x1f558; 1.2 fork的返回值问题&#x1f558; 1.3 写时拷贝&#x1f558; 1.4 创建多个进程&#x1f552…...

HummerRisk V0.9.1:操作审计增加百度云,增加主机检测规则及多处优化

HummerRisk V0.9.0发布&#xff1a;增加RBAC 资源拓扑图&#xff0c;首页新增检查的统计数据&#xff0c;云检测、漏洞、主机等模块增加规则&#xff0c;对象存储增加京东云&#xff0c;操作审计增加金山云&#xff0c;镜像仓库新增设置别名。 感谢社区中小伙伴们的反馈&#…...

Rust入门(十六):手写web服务器和线程池

这一章将实现一个手写的 web server 和 多线程的服务器&#xff0c;用到之前学到的所有特性 简单的web server 作为一个 web 服务器&#xff0c;我们首先要能接收到请求&#xff0c;目前市面上的 web 服务大多数都是基于 HTTP 和 HTTPS 协议的&#xff0c;而他们有是基于 TCP…...

数据结构——第二章 线性表(1)——顺序结构

线性表1. 线性表1.1 线性表的定义1.1.1 访问型操作1.1.2 加工型操作1.2 线性表的顺序存储结构1.2.1 定义顺序表数据类型方法11.2.2 定义顺序表数据类型方法21.3 顺序表的基本操作实现1.3.1 顺序表的初始化操作1.3.2 顺序表的插入操作1.3.3 顺序表的删除操作1.3.4 顺序表的更新操…...

YOLO 格式数据集制作

目录 1. YOLO简介 2.分割数据集准备 3.代码展示 整理不易&#xff0c;欢迎一键三连&#xff01;&#xff01;&#xff01; 1. YOLO简介 YOLO&#xff08;You Only Look Once&#xff09;是一种流行的目标检测和图像分割模型&#xff0c;由华盛顿大学的 Joseph Redmon 和 Al…...

基于linux内核的驱动开发

1 字符设备驱动框架 1.1字符设备 定义&#xff1a;只能以一个字节一个字节的方式读写的设备&#xff0c;不能随机的读取设备中中的某一段数据&#xff0c;读取数据需要按照先后顺序。&#xff08;字符设备是面向字节流的&#xff09; 常见的字…...

找不到工作的测试员一大把,大厂却招不到优秀软件测试员?高薪难寻测试工程师。

测试工程师招了快一个月了&#xff0c;实在招不到合适的&#xff0c;已经在被解雇的边缘了。。。” 初级测试工程师非常多&#xff0c;但真正掌握测试思维、能力强的优秀测试太少了&#xff01; 据我所知&#xff0c; 当下的测试人员不少状态都是这样的&#xff1a; 在工作中…...

buuctf Basic

buuctf Basic 1.Linux Labs 根据提示我们可以知道需要远程连接linux服务器&#xff0c;这里使用xshell进行如下配置 输入ssh的用户名root&#xff0c;密码123456 连接成功 构造命令 ls …/ 查看文件 查看flag cat …/flag.txt 为flag{8fee8783-1ed5-4b67-90eb-a1d603a0208…...

赛狐ERP|亚马逊产品缺货怎么办?该如何补救?

由于物流时效的延长&#xff0c;运输成本的增加&#xff0c;亚马逊的仓储限制等各种原因&#xff0c;断货问题很常成为亚马逊卖家的普遍困扰。那么亚马逊产品缺货应该怎么办&#xff01;1、提高产品价格&#xff1a;除了卖自己的Listing此外&#xff0c;提高产品价格也是一种保…...

《Elasticsearch源码解读与优化实战》张超-读书笔记

写在前面 好久没更新博客了&#xff0c;应届狗没办法啊╮(╯▽╰)╭为了秋招搞了小半年&#xff0c;从去年5月到现在搞了两段实习&#xff08;京东、游戏公司&#xff09;&#xff0c;最终年前拿到一家还不错的offer&#xff0c;现在已经入职实习了&#xff0c;不出意外的话以…...

编码踩坑——运行时报错java.lang.NoSuchMethodError / 同名类加载问题 / 双亲委派【建议收藏】

本篇介绍一个实际遇到的排查异常的case&#xff0c;涉及的知识点包括&#xff1a;类加载机制、jar包中的类加载顺序、JVM双亲委派模型、破坏双亲委派模型及自定义类加载器的代码示例&#xff1b;问题背景业务版本&#xff0c;旧功能升级&#xff0c;原先引用的一个二方包中的du…...

软件测试选Python还是Java?

目录 前言 1、先从一门语言开始 2、两个语言的区别 3、两个语言的测试栈技术 4、如何选择两种语言&#xff1f; 总结 前言 对于工作多年的从业者来说&#xff0c;同时掌握java和Python两门语言再好不过&#xff0c;可以大大增加找工作时的选择范围。但是对于转行的人或者…...

“2023数据安全智能化中国行”活动,开幕即高能

工信部等16部门近日发布的《关于促进数据安全产业发展的指导意见》提出&#xff0c;到2025年&#xff0c;数据安全产业基础能力和综合实力明显增强&#xff0c;数据安全产业规模超过1500亿元&#xff0c;年复合增长率超过30%。到2035年&#xff0c;数据安全产业进入繁荣成熟期。…...

机器人操作规划——Deep Visual Foresight for Planning Robot Motion(2017 ICRA)

1 简介 model-based RL方法&#xff0c;预测Action对图像的变化&#xff0c;以push任务进行研究。 采用完全自监督的学习方式&#xff0c;不需要相机标定、3D模型、深度图像和物理仿真。 2 数据集 采用几百个物体、10个7dof机械臂采集了包括5万个push attempts的数据集。 每…...

go 连接redis集群

最近用redis shake做redis数据迁移&#xff0c;由于redis提供的客户端没有用于查看集群的工具&#xff0c;且我部署的redis集群是基于k8s来构建的&#xff0c;没有使用ingress做转发&#xff0c;所以只能在k8s内部访问集群&#xff0c;于是我先用gogin框架编写了访问redis集群的…...

LeetCode 146. LRU 缓存

原题链接 难度&#xff1a;middle\color{orange}{middle}middle 题目描述 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCacheLRUCacheLRUCache 类&#xff1a; LRUCache(intcapacity)LRUCache(int capacity)LRUCache(intcapacity) 以 正整数 …...

【mac】在m2 mbp上通过Parallels Desktop安装ubuntu22.04

文章目录前言一、参考文章二、版本信息三、方法1:通过ubuntu官网提供的iso安装3.1 配置服务器3.2 安装图形界面四、方法2:通过Parallels Desktop提供的安装包五、 小工具5.1 调整应用栏图标大小5.2 ubuntu获取mac的剪切板5.3 调整terminal字体大小5.4 安装samba5.5 ubuntu连接m…...

C++类和对象,初见类

坚持看完&#xff0c;结尾有思维导图总结 这里写目录标题C语言和 C 的区别类的定义类的初认识类的内容访问限定符类的作用域类的实例化类中的 this 指针总结C语言和 C 的区别 C 的祖师爷除了在 C语言的基础上化简了一些复杂操作 更为重要的是&#xff0c;两个语言实现的过程是…...

Redis常用数据结构及应用场景

1.总体结构 Redis中的数据&#xff0c;总体上是键值对&#xff0c;不同数据类型指的是键值对中值的类型。 2.string类型 Redis中最基本的类型&#xff0c;它是key对应的一个单一值。二进制安全&#xff0c;不必担心由于编码等问题导致二进制数据变化。所以redis的string可以…...

C++虚继承内存布局

C菱形继承内存布局 编译器&#xff1a;Visual Studio 2019 关于如何查看内存布局 B class B { public:B(): _ib(10), _cb(B){cout << "B()" << endl;}B(int ib, char cb): _ib(ib), _cb(cb){cout << "B(int,char)" << endl;}vi…...

IO模型--从BIO、NIO、AIO到内核select、poll、epoll剖析

IO基本概述 IO的分类 IO以不同的维度划分&#xff0c;可以被分为多种类型&#xff1b;从工作层面划分成磁盘IO&#xff08;本地IO&#xff09;和网络IO&#xff1b; 也从工作模式上划分&#xff1a;BIO、NIO、AIO&#xff1b;从工作性质上分为阻塞式IO与非阻塞式IO&#xff1b…...

Zebec完成BNB Chain以及Near链上协议部署,多链化进程加速

从去年开始&#xff0c;Zebec 就开始以多链的形式来拓展自身的流支付生态&#xff0c;一方面向更多的区块链系统拓展自身流支付协议&#xff0c;即从Solana上向EVM链上对协议与通证等进行迁移与拓展。目前基本完成了在BNB Chain以及Near上的合约部署&#xff0c;且能够在这些EV…...

wpscan常见的使用方法

目录 简单介绍 暴力破解 信息收集 指定用户爆破 命令集合 简单介绍 Wordpress是一个以PHP和MySQL为平台的免费自由开源的博客软件和内容管理系统。 WPScan是Kali Linux默认自带的一款漏洞扫描工具&#xff0c;它采用Ruby编写&#xff0c;能够扫描WordPress网站中的多种安…...

Tree 底层源码实现(二叉树、递归、迭代)

树&#xff08;Tree&#xff09;是一种非线性数据结构&#xff0c;由一组节点和它们之间的边组成。在树中&#xff0c;每个节点都有零个或多个子节点&#xff0c;除了根节点外&#xff0c;每个节点都有且仅有一个父节点。树可以被用于许多应用程序&#xff0c;如文件系统、XML文…...

家政服务小程序实战教程13-接入客服

小程序在微信里使用&#xff0c;以其无需安装随用随走为特点。但是有个问题是&#xff0c;如果提供商品或者服务的&#xff0c;用户如果有问题往往希望平台的运营方给出专业的解答。为了满足这类需求&#xff0c;就需要我们提供客服接入的功能&#xff0c;用户可以点击客服图标…...

大白话高并发(三)

背景 高并发得第三篇&#xff0c;讲一讲压测吧&#xff0c;因为我的目的是模拟100万人同时来秒杀。 是不是真的要找100万个人 没必要 &#xff0c;你就算100万人掐着表在同一毫秒内把请求请求某一台机器&#xff0c;服务器也不可能在同一时间处理那么多请求&#xff0c;因为…...

vue全家桶(四)前端工程化

vue全家桶&#xff08;四&#xff09;前端工程化1.模块化的相关规范1.1模块化概述1.2模块化的分类A.浏览器端的模块化B.服务器端的模块化C.ES6模块化1.2.1 Node.js中通过bable体验ES6模块化1.2.2 ES6模块化的基本语法1.2.2.1 默认导出与默认导入1.2.2.2 按需导出与按需导入1.2.…...

超螺旋滑模控制(STA)

超螺旋滑模控制(Super Twisting Algorithm, STA) 超螺旋滑模控制又称超扭滑模控制&#xff0c;可以说是二阶系统中最好用的滑模控制方法。 系统模型 对于二阶系统可以建立具有标准柯西形式的微分方程组 {x˙1x2x˙2fg⋅u\begin{cases} \dot x_1 x_2 \\ \dot x_2 f g \cdo…...

NX二次开发编译时dll自动数字签名及拷贝

前言 在UG5.0开始&#xff0c;所有基于UG二次开发的DLL都要“签名”后才能被客户端上正版的NX调用。 一、基于C# 开发签名 1、添加资源文件 &#xff08;1&#xff09;项目类库上右键–>属性–>资源–>添加资源右边小三角–>添加现有文件–>切换到UG安装目录下…...

教你如何搭建人事OA-薪资管理系统,demo可分享

1、简介1.1、案例简介本文将介绍&#xff0c;如何搭建人事OA-薪资管理。1.2、应用场景根据设置薪资基础及考勤和绩效的数据计算得到各个员工工资详情。2、设置方法2.1、表单搭建1&#xff09;新建表单【工资表】&#xff0c;字段设置如下&#xff1b;名称类型名称类型人员资料分…...

做网站的去哪找客户/百度seo排名教程

化到这一步&#xff0c;我们就将原来一个数据会非常大的A^B,变成了很多项的乘积。编程实现的时候&#xff0c;我们只需走一遍B的二进制位&#xff0c;并用一个变量a记录当前二进制位的权值&#xff0c;判断当前bi的值&#xff0c;然后将结果乘起来取模即可。快速幂取模通过将指…...

网站新闻对百度优化有用吗/常德论坛网站

很简单的话题。当我听到有人在讨论自己实现机制控制 log 输出时&#xff0c;我觉得还是有必要记录一下。最近让我比较困扰的是&#xff0c;很多 Android 基本的技巧都不被知晓。许多人的“锤子”意识很严重&#xff0c;一直使用以往的经验处理一切问题。影响 Android log 输出的…...

wordpress个性/一般开车用什么导航最好

本文是对《【硬刚大数据之学习路线篇】从零到大数据专家的学习指南(全面升级版)》的ES部分补充。...

其它区便宜营销型网站建设/宁波seo搜索优化费用

eclipse界面定制&#xff0c;让eclipse看着更清爽转载于:https://www.cnblogs.com/passer1991/p/3227755.html...

网站开发职位介绍/优化推广网站seo

Android Webview虽然提供了页面加载及资源请求的钩子&#xff0c;但是对于h5的ajax请求并没有提供干涉的接口&#xff0c;这意味着我们不能在webview中干涉javascript发起的http请求&#xff0c;而有时候我们确实需要能够截获ajax请求并实现一些功能如&#xff1a;统一的网络请…...

wordpress 漏洞扫描/pc网站建设和推广

新手想半天不会写,必须用二维数组吗?我想求面试成绩和笔试成绩的总分和平均分,大神给个思路!!!//下面是我创建的StudentScore类using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace MyScore{class St…...