Linux多进程

 


父子进程

一个进程可以创建另一个进程吗?

干想代码可能很犹豫,因为我们从来都没有见过创建新进程的代码。但是:

短短几秒钟,就创建了两个进程。这些进程是系统创建的吗?不是。一个是小爱同学创建的,另一个则是QQ创建的。所以,一个进程是完全拥有创建另一个进程的能力的。

就像你和你的室友们一样,被创建的进程就叫做子进程,另一个就叫做父进程。 

创建父子进程 

但是,小爱同学和QQ也是代码写出来的,那怎么用代码去创建一个进程呢?很简单的一个函数
fork()
不过,使用fork的代码就很反人类了: 

int main()
{int pid = fork();if(pid==0){printf("this process:%d\n",pid);}else{printf("And this process:%d\n",pid);}return 0;
}

很简单啊,判断pid的值,不管是不是0,最后都输出自己的pid,无非就是输出pid前的语句不同罢了。
可是结果:

嗯?怎么会输出两条语句?
而且两个输出的pid还不一样? 

中途没有任何语句去改变pid的值,而且就算改变了,一个程序运行时,可能同时进入if和else两条路吗?不可能,除非这个入开桂了。
此时,我们再来想想这个函数的功能:创建一个子进程。那么,只剩下一种可能: 

那我们就以这个假设来看这段代码。
因为整段代码,只有在fork处改变了pid的值,所以肯定是fork对pid发生了不同的改变。对两个不同的进程,会分别给他们返回不同的值0或非0,然后把两个值分别赋值给不同进程中的pid,继续向后运行。此时,进入if和else的判断,因为pid的值是0或非0,所以输出了不同的结果。完美! 

但是,又会产生很多问题:

  1. fork返回值的具体含义是什么?为什么要返回0或非0?
  2. 我们怎么判断谁是父进程?谁是子进程?重要吗?
  3. 子进程凭什么可以进入到if/else的判断里? 

别急,我们来一个一个分析: 


fork 

fork返回值的含义是什么? 

一句话总结:fork返回值的具体含义,就是某个进程的pid。 

pid的全程,process id,就和你元神的uid一样,只是为了可以找到你。这个在进程中有讲过,不理解的可以去看看:Linux进程与运行-CSDN博客 

更具体的说,fork返回的是子进程的pid。父进程创建子进程,就把创建出的子进程pid给他。但是子进程刚被创建出来,还没有创建进程,他没有子进程,怎么办?因为进程的计数是从1开始的,1是第一个被创建出来的进程,也就是操作系统。那我就返回一个0,就如同返回一个null一样,代表没有子进程。于是,fork函数的返回值就明了了:

  • 给父进程返回刚被创建的子进程的pid
  • 给子进程返回0 

但是,你怎么证明给父进程返回的,就是子进程的pid?
很简单一个指令:ps

ps,简单说就是任务管理器,可以查看所有进程和其pid 

同时,在代码内,也有相应的函数:getpid()和getppid()

  • getpid(),就是获取当前进程的pid
  • getppid(),就是获取当前进程的父进程pid,ppid(parent pid) 

所以,我们再把刚刚的代码稍微改一下:

int main()
{int pid = fork();if(pid==0){printf("I'm child,my id:%d\n",getpid());printf("my child id:%d\n",pid);printf("my parent id:%d\n\n",getppid());}else{sleep(1);//为了防止输出串行,让父进程后运行printf("I'm parent,my id:%d\n",getpid());printf("my child id:%d\n",pid);printf("my parent id:%d\n\n",getppid());}return 0;
}

很容易发现父子关系。
而且,因为子进程没有孩子,所以孩子pid为0;父进程是另外一个进程创建的,所以给的是创建父进程的进程pid。
OK,那么第一个问题便解决了:

怎么判断父子进程?

其实在上一个问题中,就已经解决了怎么判断父子进程:getpid和getppid。那么,这里重点讲一下第二个问题:
判断谁是父子进程,重要吗? 

废话,当然重要。我们先不管具体怎么实现,我们只先来思考一下这些概念:

首先,我们为什么要去创建一个子进程?因为是为了让他完成某些任务。那我们怎么知道这个任务完成地怎么样?子进程去告诉父进程。所以,子进程一定要可以找到父进程,父进程也一定要能找到子进程。
其次,如果子进程在完成之前,父进程意外终止了,那子进程的结果和状态,应该给谁呢?只能给其他相关的进程,让能处理这些东西给父进程擦屁股的进程,去处理这些事情。所以,其他进程也必须要找到子进程,而且知道,这个子进程是失去父亲的进程。
这种父子关系是非常重要的。 

不过,这也说明了一个问题:

父进程一直在偷窥自己的子进程,等到子进程运行完了,直接冲过去拿结果。但是,父进程难道是去看子进程运行到哪一行了吗?不现实,因为父进程也要有自己的事,要是一直去盯着子进程,那父进程就不叫父亲而叫监工了。为了方便和高效,所以直接用一个状态来表示,子进程在运行的时候,就把状态设置成为running,而在进程结束的同时,把状态的running调整为finish。 

父进程偷窥的时候,只用看一眼子进程的状态,如果是running,哦,我的孩子正在运行,那我就不管了;但是,一旦发现是finish,那我就直接冲过去拿结果,极大提高了父进程的效率。

而这些,会涉及到两个问题:进程等待和进程状态。在讲完第三个问题“子进程的运行”时,我们再回来解决这两个问题。

子进程是怎么运行的 

子进程凭什么可以进入到if/else的判断里?
或者换句话说,子进程被创建之后,他运行的是什么代码? 

首先,父子进程是不可能同时运行同一段代码的,因为会涉及到一个概念——
进程的独立性:不同的进程是无法看到其他进程里的数据的。 

进程的独立性 

对每一个进程,都有一个进程PCB,PCB里存储着两个重要的东西:

struct PCB
{Data* data;//数据段地址,比如上下文等Code* code;//代码段地址,运行时的代码//还有其他数据
};

在程序被新创建的时候,PCB被放入内存中,操作系统再根据PCB,找到对应的代码和数据,将代码和数据放入内存。而在这个过程中,是没有任何其他进程插手的。换句话说,任何进程就算是父进程,对一个其他的进程,也只有获得其PCB的能力,而无法查看其中的代码和数据。
进程就像紧锁的房间,你只能从门卡上获取房间的基本信息,但是里面居住的人,里面的设施,只有房间的主人才知道,其他人没有任何办法看到。 

那父进程获取子进程的消息,既然只能看到PCB,那怎么可能获得到消息呢?
答案很简单:中间渠道。 

 

当子进程完成的时候,我不让父进程进到我房间里去,我放一张纸条在门口,父进程发现门口有张纸条,就只将纸条拿回到父进程的房间,这样父子进程在没有进出相互房间的情况下,进行了信息的交流,而这张纸条,就是实现的中间渠道。

同时,为了保证这张纸条不被其他进程拿走,或者不被操作系统打扫走,这张纸条的位置肯定要被单独保管起来,也就是操作系统给他分配一块空间,专门用于两个进程之间的交流。

进程的独立性,总结便是,每一个进程都是绝对封闭的,任何进程都无法查看其他进程的代码和数据,而进程间需要交换数据,则必须通过中间媒介来间接交换。 

所以,子进程是看不到父进程的代码的,自然就无法和父进程运行同一块代码。不过,虽然我看不见,那我就不能把代码复制过来,变成自己的代码吗? 

当子进程被创建的时候,PCB还在形成。这个时候,PCB中的数据段地址和代码段地址都是空的,但是我们想让子进程接着父进程运行,于是我们把父进程数据段和代码段的地址全部赋值给子进程,因为只是两个地址的复制,所以要的时间几乎为0。
复制之后,子进程和父进程分裂开来,两个进程保持着独立性,但是因为子进程已经把父进程的代码段和数据段地址复制来了,可以直接访问到代码段和数据段。此时就算我看不到父进程的数据,我也有着自己复制来的数据,可以自己独立运行,这就是子进程创建的过程。 

所以,子进程是怎么运行的?在fork处分裂开来,子进程复制父进程所有的代码和数据,两个进程根据自己独立的数据运行下去,互补干扰。 

以下面一段代码为例:

#include<unistd.h>
#include<stdio.h>int main()
{int tmp=0;int pid = fork();if(pid==0){tmp=1;}else{tmp=2;}return 0;
}


进程状态 

在一般的教材里,进程一般都会有一个状态转换图:

对硬件层的理解,这个图是完美的时间片逻辑:

在一个cpu上,进程是不可能占着cpu不走的,所以每隔一段时间,cpu就要从进程之间切换,由一个进程切换到另一个进程。但是,被切换走的进程并不是不能继续运行了,而是被强迫剥离走,它还想被继续运行下去,它就要在运行队列里继续排队,等到下一次被调度。在运行的时候,就是执行状态,而在运行队列里排队的时候,就是就绪状态。

但是,并非每个进程都是一直在等待着被调度的。有些进程缺少了运行的必要条件,比如:


在没有输入的时候,进程会一直卡在scanf那里,这便是进程缺少了继续运行的必要条件。这个时候,就算调度了它把它放在了cpu上,它也只能像人机一样发呆,纯纯浪费资源。为了避免这种事情发生,我们单独用一种状态表示他,当cpu看到这个状态时,明白他不能继续运行,所以直接跳过他,而这种状态,就叫做阻塞。

很完美是不是?但是这只是硬件层的理解,说人话便是给cpu看的状态。而给进程和操作系统看的状态,与之大不一样,就像:

在Linux中,有以下几个进程状态:

static const char* task_state_array[]=
{"R",//running 运行"S",//sleeping 睡眠"D",//disk sleep 深度睡眠"T",//stop 暂停"t",//tracing stop 挂起暂停"x",//dead 死亡"z"//zombie 僵尸
};

还是一个一个来看。 

运行 

并非在cpu上跑的进程状态才叫运行,只要在运行队列中,都叫做运行状态。

睡眠 

int main()
{sleep(100);
}

在sleep的100秒中,就叫做进程正处于睡眠状态。 

深度睡眠 

如果只是单单用代码sleep一段时间,叫做浅度睡眠。那么深度睡眠是什么? 

在一个进程涉及到文件操作的时候,数据量一大起来,读取文件肯定是很慢的。但是,如果在读取文件的时候,读取到一半,你把这个进程关了,那就会造成一个结果: 

如果直接把这些数据直接丢掉不管了,一些小游戏存档还好,要是是银行更新数据更新一半,操作系统直接给更新进程杀了,那: 

所以,深度睡眠状态是什么?就是因为在进行较慢的IO操作,此时进程停在了代码的某一行。但是为了保护IO操作,进程会处于深度睡眠状态,任何操作包括操作系统强制都无法杀掉深度睡眠的进程,一直到IO操作结束退出深度睡眠状态。 

除非你关机。 

 暂停

在Linux当中,我们可以通过发送SIGSTOP信号使进程进入暂停状态(stopped),发送SIGCONT信号可以让处于暂停状态的进程继续运行。

对于信号的部分,我们放在单独一个章节来讲,但是可以通过kill -l指令,来查看所有的信号: 

挂起暂停 

挂起 

什么是挂起?

我们知道,再nb的cpu,他的内存也是有上限的。当内存被塞满了,怎么办? 

  • 删掉一部分进程?不可能,当你删一个进程的时候,那个进程肯定不服气:凭什么要删我不删他?
  • 把进程压缩?也不可能,压缩和解压也是需要时间和空间的,得不偿失。 

此时,我们把头一瞥,突然发现了一件事:

所以,cpu有可能把内存中的数据放在磁盘上吗? 

之前在冯诺依曼体系中说过,内存和磁盘的空间,最大的区别便是读写速度。内存中的数据是处理最快的,所以必须要将磁盘中的数据加载到内存里,再进行操作;磁盘的空间是最大的,所以只需要保存数据的时候,就把数据放回到磁盘上。

那,是所有的数据都要被处理吗?当一个进程被暂停或者睡眠的时候,或者就算是在运行队列中没有被调度的时候,这些数据都不会被处理,而是在等待调度。既然不会被处理,那是不是就可以被暂时放回到磁盘中呢?当然可以。我们只把PCB留在内存里,把数据和代码都放回到磁盘中,等到被调度的时候,把数据和代码从磁盘里重新导入到内存里;时间片结束了,再把数据和代码放回磁盘。这样,同一时间,只有被调度的进程数据和代码在内存中,便极大节省了空间,再也不怕太多进程导致内存爆炸了。

不过,内存和磁盘的交互还是太慢了。为了提速,磁盘中单独弄出一个分区,叫做swap区(不是swag!)。swap分区大概只有2G左右,专门用于与内存的资源交互,因为swap分区很小而且地址固定,所以相比于在1T的磁盘中查找资源,在2G的固定空间中查找显然快得多。

这个时候再想想,一个大游戏,动不动就50个G,但是一般的电脑都是16G的RAM,难道游戏运行的时候,就是将所有的50G全部加载到内存里吗?当然不是,只在需要的时候,加载一个场景里的东西,当切换场景的时候,就将前一场景的内存全部除去,将新场景的内存加载,因为场景之间的切换涉及到了内存和磁盘的资源交换,现在知道为什么会这么卡了吧

 

那么,挂起是什么?PCB在内存中,但是数据和代码在磁盘中的进程,就叫做挂起状态。 

挂起和挂起暂停其实是一回事,只不过是存储的方式不同而已。 

死亡 

一个进程结束,返回信息交给父进程,从内存中消失,就叫做死亡状态。死亡是一瞬间的,并不像可碧一样世人会牢牢记住他伟大,一个进程甚至没有一个名字,一旦死亡,便无法被发现。 

僵尸 

但是,我说但是,牢大复活赛打赢了呢? 

僵尸进程,就是当子进程的任务完成了,要返回了,写了张纸条给父进程,突然发现,父进程似了。

也不一定必须是父进程死亡。有可能父进程处于暂停状态,睡眠状态,或者单纯是父进程忘了写接收子进程消息的代码,反正最后会产生一个结果:子进程虽然结束,但是无法有效告知子进程现在的状态。子进程必须要等父进程拿到消息才可以安稳趋势,但是在父进程没有接收到消息之前,子进程会处于一种状态:

而在子进程运行完毕,到父进程拿到子进程结果的中间状态,就叫做僵尸状态。 

僵尸进程是一种无法选中的状态。任何操作都无法杀掉一个僵尸进程,只有子进程把消息给父进程,僵尸进程才会自动转换为死亡进程,否则僵尸进程会一直停留在内存里,一直占用内存,某种意义上造成内存泄漏。 

孤儿进程 

父进程死亡了的进程,就叫做孤儿进程。 

对僵尸进程来说,如果父进程只是睡着了,或者暂停了,那其实还好说。因为总有一个时候,父进程会醒过来,拿到子进程的状态,然后子进程安稳趋势。但是,如果父进程忘了写接收子进程消息的代码,那问题就大了:
父进程完全忘了自己还有个子进程,父进程运行完了,直接倒头趋势;这个时候子进程一看,父进程没了!子进程变成了孤儿进程,再也没有办法把消息交给父进程,子进程便一直处于孤儿状态,没有任何办法处理掉这个进程,这块内存就无法释放了。 

所以,操作系统有专门处理孤儿进程的方法——转递抚养权。当父进程似了,子进程的抚养权就会交给父进程的父进程。但是,如果父进程的父进程也无法处理这个子进程的消息,就会再往上递,一直层层上递给能处理消息的父进程。并且,所有的进程,其最终的祖宗进程是操作系统,操作系统一定有处理僵尸进程的办法,也就保证了,任何孤儿进程的僵尸状态一定能保证被处理。 

进程等待

那么,子进程的消息,父进程应该怎么处理呢?
两种方法:wait和waitpid 

  • wait便是简单粗暴,只要有子进程返回信息,就接收信息;
  • 而waitpid,则是wait功能的扩展,所以我们重点来讲waitpid 

status 

参数中的status是进程退出状态。我们知道int的大小是4子节32byte,而传入指针是因为函数中要修改传图的status参数,像C++中传引用一样。其中32byte,并非都会用到,而只使用了低16byte

当进程正常终止的时候,就会用8-15byte,来表示退出的状态;当进程异常终止的时候,就会用0-7byte来表示终止的错误码。而我们获取status的含义,并不需要采用位操作手动分析,只需要调用两个系统函数:

bool WIFEXITED(int status); 
//若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)int WEXITSTATUS(int status); 
//若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

这些函数会自己进行位操作的分析,然后提取出有用的信息,以基本的类型表示出来。

options 

options,是进程等待的状态。进程等待有两种状态——阻塞等待和非阻塞等待。

  • 阻塞等待,就是在受到子进程的返回值之前,会一直停在代码的那一行,一直到接收到子进程的返回值再继续往后运行。
  • 而非阻塞等待,便是在waitpid处看一眼,如果子进程返回了,就拿到结果;如果子进程没有返回,也不等他,接着往后运行。所以,非阻塞等待一般会搭配一个循环运行: 
int pid=0,cnt=0;
while(pid==0)
{pid = waitpid(-1,nullptr,WNOHANG);//非阻塞等待,如果等到了子进程,就返回子进程的pid,否则返回0cnt++;
}

就像你突然肚子疼,要跑去WC,发现所有坑位都满了。
如果这个时候你很急,你要一直等在原地,当哪个坑位出来了人,你直接冲进去,就叫做阻塞等待;
如果这个时候你没有那么急,WC太臭了,你决定先出去透透气,隔段时间回来看一次坑位,就叫作非阻塞等待。 


进程退出

子进程一定是正常退出的吗?当然不是,进程可能在中间出现了某些问题,无法继续运行下去,提前退出了。但是,对于主函数退出很简单,只需要return 1便可以;对非主函数的退出,return可起不到效果: 

void func(int i)
{if(i==-1)return 1;//这里的return是返回值,并不会使程序终止
}int main()
{func(-1);return 0;//无论i是否为-1,最终都会return 0;
}

那么,有没有办法,让程序可以在任意地方退出呢?当然有: 

exit和_exit 

在C语言,肯定看到过exit这个函数,来进行程序的退出,最终的返回值为exit中的参数。但是,Linux中,存在exit和_exit,都表示程序的退出,他们的区别是什么?

exit是C语言中的函数,而_exit是系统调用函数。只有在Linux中,_exit才会生效,而且是一种强制的退出。比如:

int main()
{printf("hello world");exit(0);//分别使用_exit()和exit()
}

我们会发现,使用exit()会输出结果,而使用_exit()并不会有任何输出。这是因为,exit()是对_exit()的封装,_exit()只是完成进程退出这一功能,而exit()除了要完成进程退出,还需要刷新缓冲区,执行析构函数等等操作,所以在使用exit()的时候,刷新了缓冲区,hello world才被输出了出来,而如果只使用_exit(),他只不过完成了退出这一操作,并没有刷新缓冲区,所以缓冲区的数据永远丢失了。换句话说:

void exit(int status)
{_flash();//刷新缓冲区_destroy();//析构//..._exit(status);//最后调用_exit()
}

exit封装了许多系统调用。

而许多Linux系统调用与C语言的函数大多都是这样。对不同的系统,其系统调用都不同,用户不可能换一个系统便去学习该系统的调用。所以C语言,将所有类似的系统调用封装成一个函数,用户只需要去学习C语言,便可以用于所有的系统之中,减少了用户的学习成本,这也是C语言存在的意义——统一所有系统的操作。


 

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

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

相关文章

CSS实现加载动画

需求&#xff1a;在开发过程中&#xff0c;我们会经常遇到一些基本的加载动画需求。这里我们简单介绍几个加载动画&#xff0c;主要是通过使用css的animation和transition的属性进行实现的动画和过渡效果。 语法&#xff1a; animation: name duration timing-function delay…

电脑录屏录音软件,拥有这3款就足够

在信息化的时代&#xff0c;电脑早已成为我们生活中的得力助手。而电脑录屏录音软件&#xff0c;就像是一位能够帮助我们捕捉到屏幕上一切的魔法师&#xff0c;用其独特的魔力&#xff0c;将屏幕上的精彩瞬间和动人声音一一捕捉&#xff0c;永久珍藏。 电脑录屏与录音并不是一…

Java | Leetcode Java题解之第238题除自身以外数组的乘积

题目&#xff1a; 题解&#xff1a; class Solution {public int[] productExceptSelf(int[] nums) {int length nums.length;// L 和 R 分别表示左右两侧的乘积列表int[] L new int[length];int[] R new int[length];int[] answer new int[length];// L[i] 为索引 i 左侧…

LabVIEW机器学习实现外观检测

介绍如何利用LabVIEW平台结合机器学习技术实现对被测样品的外观检测。详细说明了硬件选择、算法使用、操作步骤以及注意事项。 硬件选择 工业相机&#xff1a;高分辨率工业相机&#xff08;如Basler、FLIR等&#xff09;用于采集样品的图像。 照明设备&#xff1a;均匀的LED照…

亲子母婴行业媒体邀约宣发资源

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 亲子母婴行业是一个综合性的产业&#xff0c;涉及多个领域&#xff0c;包括儿童食品&#xff0c;儿童玩具&#xff0c;服装&#xff0c;洗护&#xff0c;月子中心&#xff0c;母婴护理&a…

江科大SPI教程听课笔记

原理部分我打算听江科大的课复习一下&#xff0c;代码部分工作大概率用HAL库敲了。 SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线。 硬件资源方面需要四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO (Master Input Slave…

苏州大视通特种镜头规格书-360内度壁镜头-型号DST36023066

360内壁检测镜头 产品特点 专为内壁检测开发的特种镜头&#xff1b;可实现大视场角的内壁检测&#xff1b;最大支持直径 5mm-120mm&#xff0c;深度 4mm-120mm的内孔检测&#xff1b;最大支持 2/3”相机&#xff0c;成像圈为 φ6mm&#xff1b;可定制光阑孔径以调节亮度和景深…

【FPGA】优雅地使用ILA进行FPGA硬件调试

Vivado&#xff08;Vitis&#xff09;版本&#xff1a;2020.2FPGA开发板&#xff1a;Microphase Z7-Lite 7020开发板 FPGA设计调试流程 FPGA开发是一个不断迭代的过程&#xff0c;一般的FPGA设计流程一般包含下面几个步骤&#xff1a; 硬件架构和算法验证&#xff1a;实现需要…

CV每日论文--2024.7.16

1、Radiance Fields from Photons 中文标题&#xff1a;光子的辐射场 简介&#xff1a;神经辐射场(NeRFs)已成为从多个视角捕获的图像进行高质量视图合成的事实标准方法。然而,在野外环境下捕获图像时,仍存在许多挑战,例如低光、高动态范围或快速运动导致的模糊重建和明显的伪…

Apache trino的ldap认证开启

作者&#xff1a;櫰木 1、背景 由于trino 默认没有开启用户认证体系&#xff0c;需要ldap用户进行认证。开启tls和ldap用户认证&#xff0c;提高安全性。 2、配置 前置条件。 trino 集群已经部署完成 ldap 服务 openjdk 版本大于11.0.17 生成证书 keytool -genkeypair…

前端pc和小程序接入快递100(跳转方式和api方式)====实时查询接口

文章目录 跳转方式微信小程序&#xff08;我以uniapp为例&#xff09;pc api接入说明关于签名计算成功示例 跳转方式 没有任何开发成本&#xff0c;直接一键接入 可以直接看官方文档 https://www.kuaidi100.com/openapi/api_wxmp.shtml 微信小程序&#xff08;我以uniapp为例…

SQL,python,knime将数据混合的文字数字拆出来,合并计算(学习笔记)

将下面将数据混合的文字数字拆出来&#xff0c;合并计算 一、SQL解决&#xff1a; ---创建表插入数据 CREATE TABLE original_data (id INT AUTO_INCREMENT PRIMARY KEY,city VARCHAR(255),value DECIMAL(10, 1) );INSERT INTO original_data (city, value) VALUES (上海0.5…

十一、Java多线程(模块16-17)

十一、Java多线程 day08-线程安全,死锁,状态,通讯,线程池今日目标1 线程安全1.1 线程安全产生的原因注意 : 以上代码是有问题 , 接下来继续改进通过上述代码的执行结果 , 发现了出现了负号票 , 和相同的票 , 数据有问题 1.2 线程的同步1.3 同步代码块1.4 同步方法1.5 Lock锁 2 …

电脑QQ录屏,2个方法轻松搞定电脑录屏

“电脑QQ也有录屏功能吗&#xff1f;我使用电脑QQ多年居然不知道QQ还有录屏功能。电脑QQ录屏功能怎么使用啊&#xff1f;有没有会的小伙伴可以教教我&#xff1f;先谢谢大家啦&#xff01;” QQ作为陪伴我们多年的“老朋友”&#xff0c;它不仅是我们与朋友家人沟通的重要桥梁…

揭秘OpenCV:探寻视觉世界的轮廓之谜

揭秘OpenCV&#xff1a;探寻视觉世界的轮廓之谜 1. 介绍1.1 什么是轮廓检测&#xff1f;1.2 OpenCV 中的轮廓检测应用 2. 准备工作2.1 安装 OpenCVWindows 用户:macOS 用户:Linux 用户: 2.2 加载图像2.3 图像处理&#xff08;转换图像颜色空间&#xff09;2.4 表格描述 3. 轮廓…

对某根域的一次渗透测试

前言 两个月之前的一个渗透测试项目是基于某网站根域进行渗透测试&#xff0c;发现该项目其实挺好搞的&#xff0c;就纯粹的没有任何防御措施与安全意识所以该项目完成的挺快&#xff0c;但是并没有完成的很好&#xff0c;因为有好几处文件上传没有绕过&#xff08;虽然从一个…

前端基础之Flex布局

【图书推荐】《HTML5CSS3 Web前端开发与实例教程&#xff08;微课视频版&#xff09;》-CSDN博客 Flex布局概述 Flex布局全称为Flexible Box布局模式&#xff0c;是CSS3规范中一项革命性的强大布局技术。它以“弹性”为核心理念&#xff0c;旨在提供一种更为灵活且功能丰富的…

论文复现丨物流中心选址问题:蜘蛛猴算法求解

路径优化系列文章&#xff1a; 1、路径优化历史文章2、物流中心选址问题论文复现丨改进蜘蛛猴算法求解 物流中心选址问题 一般物流中心选址问题是指&#xff1a;在有限的用户(即需求点)中找出一定数量的地点建立配送中心&#xff0c;实现从物流中心到用户之间的配送&#xf…

如何追查一个packet在linux 系统哪里丢失

要想追一个包在系统哪里丢失了&#xff0c; 就要了解 一个应用层的包在送出时 要经历那些 检查点 和被丢掉的点。 1. 在传输层&#xff0c;如果是 tcp 包 会有contrack 的 buf 的限制 可能会导致 packets 的丢失。 > 检查办法&#xff1a;查看dmesg日志有报错&#xff1a;k…

类和对象——对象的初始化和清理

本文为观看 C黑马程序员 的学习笔记。 构造函数和析构函数 构造函数&#xff1a;进行初始化操作 析构函数&#xff1a;进行清理操作 构造函数 语法&#xff1a;类名&#xff08;&#xff09;{} 构造函数&#xff0c;没有返回值&#xff0c;也不写void函数名称与类名相同构造…