Linux教程

【Linux】三万字学会进程控制

本文主要是介绍【Linux】三万字学会进程控制,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • 前言
  • 一、进程创建
    • fork是什么
    • 为什么有两个返回值
    • 为什么要有写时拷贝
  • 二、进程终止
    • exit和_exit的区别
  • 三、进程等待
  • 四、进程程序替换
    • shell的运行原理
  • 五、PCB的组织方式
  • 总结


前言

进程的有关的概念在上面那一章已经比较清晰的,那么这一章我们具体如何调用对应的系统接口进行进程的创建,以及更加深入的理解进程。


一、进程创建

fork是什么


fork出来的子进程,以父进程为模板,很多数据,代码都继承父进程,当中的进程PCB,进程地址空间,页表是每个进程都有的,但是其中的属性数据(pid),子进程的调度时间,pid,ppid,兄弟id,与其他进程的链接关系都要重新设置,但是其他大部分信息都是以父亲为模板的。

示意图:
在这里插入图片描述

调用fork后:创建子进程,本质系统多了一个进程,多了一套与进程相关的数据结构。

调用fork后从系统层面:

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



为什么有两个返回值


当我们用pid_t pid = fork();的时候,为什么会进入不同的逻辑当中,一个变量如何会有两个不同的返回值,从而让父子进入不同的业务逻辑。这里有两种理解:

第一种理解:

fork是一个函数,在fork这个函数体当中,一定有创建pcb,创建进程地址空间,创建页表,构建pcb之间的关系(链接关系),子pcb连入调度队列中等等。在return之前子进程已经创建出来了,创建这些的动作是由父进程执行,返回值pid变量是由父进程创建的,这里不管是谁创建的,对于父子进程而言这个变量是共享的,当返回的时候相当于是对变量的写入,这个时候就会发生写实拷贝,由此父子进程当中都有自己的pid!!于是一个变量名,内容就是不同的,本质就是父子页表映射数据到了不同的物理内存区域
此时fork返回之前子进程已经创建成功,对于pid的写入就会发生写实拷贝了。

第二种理解:

这种理解不一定正确,但是便于理解,在fork函数体return时,子进程已经被创建出来了,return两次,pid就会有两份。


fork怎么调用

为什么要有写时拷贝


什么是写时拷贝:

当父/子进程修改页表属性为只读(操作系统才能知道)的物理块时,就会发生写时拷贝。修改方写时出现错误,操作系统拦截并且帮我们拷贝数据到新的内存块,帮我们修改了页表上的链接关系,修改页表中的读写权限为可读可写,这时就不会报出错误给用户层。- -这一过程由OS参与完成!
在这里插入图片描述

为什么要延时拷贝:

因为要保证父子进程的独立性,那为什么不在一开始的时候就帮子进程创建他自己的代码和数据呢?这种延时拷贝技术为什么会存在呢?
早在学习语言层的写时拷贝时,倘若在父子进程创建的时候直接将数据各自拷贝一份,也能达到父子进程的独立性。
但会带来几个缺点:

  • 调用fork的效率: fork时,创建对应的数据结构,若还有将数据拷贝一份,一定会带来fork本身效率的降低,并且当内存紧张的时候有可能失败!
  • 进行拷贝的必要性: 所有的数据,并不是父和子都会进行写入,父进程若有10G数据,若子进程压根不对父进程数据进行修改,此时不需要写入(也就是只读的),此时拷贝是没有意义,并且占用物理内存空间的,浪费系统资源(cpu资源)! --fork后
  • 调用成功的概率: fork本身就是像系统要更多的资源,要更多的资源和更少的资源,显然要更少的资源更不容易导致fork失败!

创建进程的场景:
1.命令行启动命令(程序、指令等)。
2.通过程序自身,fork出来的子进程。
并不是程序要运行一定要创建新进程!

注意: 写实拷贝在上述的中强调的是数据的写时拷贝,代码虽然不进行修改,但是也是会发生类似写时拷贝的问题,在exec系列函数中我们叙述。



二、进程终止


如何做到进程终止:

在main当中return进程终止,在任意函数体exit则进程终止。

return的理解:

我们return返回的结果是给系统看的,确认进程执行是否正确。
我们可以通过命令echo $?,他查看的是最近一次执行的程序的退出信息,当你调用这个命令成功后,由于这个命令执行起来成为一个进程,他成功后返回值也是0,所以echo一般只关心第一次。
在这里插入图片描述

main当中返回值在学习语言时经常设置成0,这是因为我们默认认为返回值是0表示进程执行成功。

return的正确运用:

实际上我们在编写代码的时候可以在不同地方,由不同的分支return不同值。代表不同的结果。当出现错误时方便我们定位问题。


程序退出的情况分类:
1.代码跑完,结果正确。 – 退出码0
2.代码跑完,结果不正确。 – 退出码可以自己设置,一般设置为!0。也可以使用系统的错误码list。
3.代码被信号杀掉了(除零错误,指针越界访问)。 退出码此时不重要了,而已也不一定准确。

由于计算机对数字敏感,而我们对字符串信息敏感,所以错误码通常都是给定一个整数转换成一个字符串。


带大家看一下错误码:共135个,使用strerror可以循环遍历打印。
在这里插入图片描述
但是并不是所有的父进程都关心子进程的退出码,后面我们会学习到有相关的系统调用让子进程运行完自动结束。

异常的理解:

异常是指控制流当中发生了突变,用来相应处理器状态中的某些变化,当CPU在执行某个指令的时候,CPU内部的状态被编码为不同的位和信号。这种状态的变化叫做事件事件的发生可能与程序正在执行的指令有关(野指针,除零,越界),也有可能是外部系统定时器产生的信号或者一个IO请求的完成。


exit和_exit的区别

exit和_exit在任何地方表示直接终止进程,但是exit会调用_exit,但是调用它之前会调用一些工作。

在这里插入图片描述
_exit不会刷新缓冲区,所以不会打印hello world,而exit会执行用户通过atexit或on_exit定义的清理函数,关闭所有打开的流,所有缓存数据均被写入,后exit函数调用_exit。

站在操作系统的角度,如何理解进程终止?
核心思想:归还资源

  • 1.“释放”曾经为何管理进程所维护的所有数据结构对象
  • 2.“释放”程序代码和数据占用的内存空间
  • 3.取消曾经该进程的连接关系

解释:

1."释放"在这里打双引号是因为操作系统并不是直接将资源释放掉,而是将资源放在一个池子,需要资源再由Slab分配器分配效率就高了。
2.释放程序代码和数据实际上只要把对应的内存设置成无效就可以,后面的程序可以直接覆盖式的使用设置为无效的内存,这样提升释放的效率。
3.取消链接关系实际上就是操作指针,进程之间的组织方式是用双链表完成的,取消两者的链接关系实际上就是对指针的操作。


三、进程等待


进程等待的必要性:

1.回收僵尸进程(kill命令杀不掉),解决内存泄漏。子进程没有父进程的等待会保留部分数据结构。
2.需要获得子进程的运行结束状态。但这个不是必须的!
3.尽量父进程要晚于子进程退出,可以规范化进行资源回收。这是一个编码上的习惯。

进程等待的方法:
wait,等待任意一个子进程,当子进程退出wait返回,是一种阻塞式的等待。
回收工作由操作系统完成,是父进程调用wait接口由操作系统完成。

wait:

wait测试的代码
在这里插入图片描述
测试结果:
在这里插入图片描述

将代码改成多进程,我们采用循环等待。
在这里插入图片描述
结果:
在这里插入图片描述

结果2:
在这里插入图片描述

注意: 当子进程僵尸了,而父进程退出后,子进程会由一号进程领养,此时ps axj命令不一定能看到该进程,因为该进程可能已经被一号进程释放了。



waitpid:

waitpid的第一个参数,当设置为-1则表示等待任意子进程,和wait等效,设置>0的值时等待该具体的pid(即等待指定的进程)。等待由父进程调用操作系统,由操作系统完成。

fork的返回值,给父进程返回的是子进程的pid,那么恰好父进程就可以拿着这个pid去等待他了。等待,本质也是管理的一种方式。

waitpid的第三个参数设置成0默认就是阻塞式等待。这种方式最简单。
以等待一个进程为例,waitpid也是比较简单,第一个参数填写对应的id即可。
在这里插入图片描述


若要等待多个,则可以用数组将要等待的pid存起来,然后遍历数组即可。由于只有一个进程在进行等待,可以采用计数器的方式,将等待的进程数记录,最后再跳出循环即可。
代码:在这里插入图片描述
结果:
在这里插入图片描述



status的理解:

正常终止的次低8位就是退出时的退出码!
在这里插入图片描述

以定义全局变量的方式让父进程看到子进程的退出码的方式不可取。
我们(status>>8) & 0xFF,让status的次八位与上全1,拿到的就是我们的退出码了。这种方式是为了验证它的stauts的构成。
代码:在这里插入图片描述
结果:
在这里插入图片描述
父进程通过拿到退出码可以再做一些处理。waitpid拿到的status是从子进程的尚未释放的资源拿到的,僵尸状态时候有部分资源是无法释放的,通过调用waitpid可以将僵尸进程的退出信息拿上来。

异常退出的情况:

一般进程提前终止,本质是该进程收到了os发送的信号。用低七位表示终止信号,用中间一位表示是否core
dump(该标志位之后叙述)。我们调用kill
-l可以发现我们普通信号当中并没有0号信号,所以当我们检测退出码的低七位全0时,我们就可以辨别是否是正常终止了。 在这里插入图片描述

正常退出时,signal为0,即未收到信号。
在这里插入图片描述

当我们在子进程编写死循环,用kill命令杀掉子进程,结果signal被设置为9,退出码这时是0。这也说明被信号所杀时我们可以不用关心退出码了,无意义。这种是外部信号导致的。
在这里插入图片描述

在这里插入图片描述

内部指针越界访问,除零错误,我们观察低七位变化。
除零时signal为8 在这里插入图片描述
在这里插入图片描述
指针越界访问的信号为11。越界不修改指针指向内容则会正常退出!
在这里插入图片描述
在这里插入图片描述


完整的进程创建的逻辑:

代码:
在这里插入图片描述

运行结果: 正常运行时
在这里插入图片描述
出现野指针时:
在这里插入图片描述

有关宏的运用:
系统提供了宏,来判断退出码和退出状态!我们可以直接用。
常用的宏:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

在这里插入图片描述



option参数的理解

阻塞:
阻塞方式简单,运用的场景也很多,谈到阻塞不得不说cpu当中还有等待队列的概念,当父进程调用waitpid的时候,操作系统更改父进程的状态R为!R,然后将父进程从运行队列当中取下,放到等待队列当中。当子进程运行结束时,操作系统会根据waitpid重新将父进程唤醒,将他的状态改为R,放入运行队列当中。在这里插入图片描述

非阻塞:
非阻塞就是轮询访问,但是本身并不会卡住,而是不断检测状态,检测进度。而对端也不一定立马就就绪。–非阻塞轮询方案。相对于阻塞会对调用方更高效。但是在单执行流当中,阻塞会比较简单,用的也会比较多。
WNOHANG(不hang住,不宕机):若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
在这里插入图片描述
此处失败两层含义:
1.并不是真的失败,仅仅是对方的状态没有达到预期,下次继续运行。
2.真的失败了,返回。
waitpid的WNOHANG有三种返回方式1.失败,下次再检测,2.成功:已经返回,3.失败:真正失败
所以它的返回值有三种。
代码:
在这里插入图片描述
测试:
在这里插入图片描述



阻塞vs非阻塞
阻塞,即父进程在等,子进程在跑,等待是将R状态弄成非R状态,放入等待队列,子进程运行结束后操作系统会把父进程唤醒等待,重新放到运行队列,设置其进程状态为R。这些工作都由OS负责。非阻塞轮询相当于一个死循环在不断向目标进程进行询问,对占用CPU资源更多

操作系统如何知道该进程对应的父进程在等待队列?
由于是我们的父进程掉了wait/waitpid到了等待队列,操作系统能够得知要等待的进程是谁,所以它是有能力将等待队列的进程唤醒的。
所以当我们上层看到电脑进程卡住了,就是可能放到了等待队列,然后操作系统处理完相应的错误然后才把等待队列的进程弄出来。


内核退出码字段:

/* task state */
	int exit_state;
	int exit_code, exit_signal;//退出码,退出信号
	int pdeath_signal;  /*  The signal sent when the parent dies  */

关于阻塞,非阻塞的更多了解:大佬的博客


四、进程程序替换


进程程序替换是什么:

就是让进程不再执行父进程的代码和数据,而是让进程执行新的代码和数据。
我们在linux下的ls,pwd等都是程序,我们通过exec*,让特定基础呢会给你去加载瓷盘中的其他程序,达到运行的目的,期间不创建新的进程。
在这里插入图片描述

创建子进程的目的,为什么要进行程序替换:

1.执行父进程的部分代码
2.执行其他程序的代码 --程序替换
子程序自身新的程序的需求。如网络当中通常让一个进程等待,若干个进程执行。
以及shell的运行原理!!!

在这里的代码实际上也会发生了类似写时拷贝来进行程序替换!!

系统如何做到重新建立映射?

当需要加载的时候,让子进程对内存的代码和数据进行写入,子进程就会使用写时拷贝,在内存中开辟内存和数据,将磁盘中的可执行文件加载到新的内存空间里。
所以用的也是写时拷贝那一套逻辑。

在进行程序替换的时候,有没有创建新的进程?

没有,因为我们不需要创建进程PCB,地址空间,页表。
并且我们的子进程的pid也没变。

程序要进行运行,一定要加载到内存!
但程序加载到内存,不一定会变成一个新的进程!

系统如何做到重新建立映射?

exec*函数。

程序执行失败就会执行后面的代码。软件被编译前要先被加载进内存,exec可以理解是特殊的加载器。软件要运行起来要先形成进程,在执行exec函数!

#include <unistd.h>`
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 execve(const char *path, char *const argv[], char *const envp[]);

函数解释:

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

记忆方式:

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量

注意:不管是以列表还是数组最后都要跟NULL
我们现在简单使用一下execl,第一个参数是绝对或者相对路径都是可以的。
l为list方式,第二个参数往后就是命令行怎么写,你就如何写,一个个的以列表的形式传入函数。

在这里插入图片描述

当我们没有创建子进程的时候我们可以发现上面的第一条和第二条代码都正确执行了,可是第三行代码却没有打印出来,这其实就是因为程序替换了之后对于2往后的代码都已经看不见了,这个时候的返回值是没有意义的,execl本身函数失败返回的是-1,我们用wait/waitpid进行进程等待的时候,拿到的退出信息是被替换的进程的退出信息。
在这里插入图片描述
由于直接在主进程进行程序替换没有意义,还不如在命令行中输入命令,所以一般我们是创建子进程,让子进程进行程序替换,而父进程进行等待或其他工作。

代码:此处验证了execl成功后不会执行下一步代码,最终父进程获得的退出码是被替换的数据和代码的执行结果。
在这里插入图片描述
测试结果:
在这里插入图片描述
当我们故意把命令打错时,他就会执行后序代码,这个时候若不想失败时执行后序代码,可以用execl的返回值来进行判断,execl失败返回-1。
在这里插入图片描述

由execv对main函数的一个理解 我们知道main函数实际上有三个参数(int argc,char* argv[],char* env[]); 它的第二个参数实际上就是系统创建进程时通过该函数将参数传入,所以这也验证了上面所说的exec*是加载器,能够让新的程序运行。


execv的测试 代码:相对于execl就是把列表展示的参数,放入数组当中然后传参。在这里插入图片描述
测试结果
在这里插入图片描述

execlp的测试 代码:看到下面的代码,有同学可能会对两个ls比较奇怪,这样做会不会重复了?
并不会,第一个参数是告诉系统去执行谁,第二个参数是告诉系统我想怎样执行。
可以不带p的exec系列函数第一个参数要解决去哪找,找谁的问题 带p的回去系统的环境变量当中找,我们只需要提供找谁的问题。
所以倘若有需要,我们可以把自己的路径导入环境列表里面,就可以使用execlp指令了,但是不推荐这样做。
第二个参数往后就是我们要执行的参数列表
在这里插入图片描述结果:在这里插入图片描述

execvp测试
代码:实际上这个时候再看这个函数就会觉得比较简单了。在这里插入图片描述
在这里插入图片描述

execle的理解:
e:可以把环境变量传递给被替换进来的程序。
用自己写的软件去调用别的程序
我们先试试用execl调用其他程序,这里的第二个参数可以不用添加路径了,因为第一个参数已经指名了路径。
proc.c:
在这里插入图片描述
mycmd.c:在这里插入图片描述
结果:
在这里插入图片描述

Makefile当中形成若干个可执行程序的方法
在这里插入图片描述

execle的使用:
传递环境变量给被替换的进程,我们可以使用我们函数体内定义的环境变量传递给子进程,也可以使用main函数体的env的环境变量。
结果:直接运行mycmd则没有该环境变量,运行proc后程序替换到my_cmd,发现my_cmd拿到了环境变量,即环境变量可以有进程传递给被替换即进来的进程。
即带e的exec函数是可以传默认或者自定义的环境变量到目标可执行程序!
在这里插入图片描述

系统调用接口execve的使用:
对比起execle区别不大,就是把参数放入数组传入即可
代码:
在这里插入图片描述
结果:
在这里插入图片描述
当我们不使用自己写的环境变量,而用系统给我们的环境变量。
代码:
在这里插入图片描述
导入系统环境变量:export MYENV=hello world
可以发现最终结果替换的进程能拿到MYENV,只不过少了后面的world,这是因为在命令行当中默认的环境变量通常是路径啥的,是不会有空格的。
在这里插入图片描述

execvpe:由于我们这里的my_cmd不在系统环境变量当中,说我们这里使用pwd获取我们的可执行文件的路径,然后PATH=$PATH:路径名的方式。
代码:在这里插入图片描述
结果:
在这里插入图片描述

实际上我们使用传环境变量场景的很少,但环境变量具有全局属性的原因,所有子进程都会继承即所有子进程通过exec*执行新程序的时候把环境变量传递给他们!!

execve和其他exec*是上下层的关系,他们最终都会调用execve来执行。

在这里插入图片描述


shell的运行原理


有部分程序只能由shell自己执行,就是对应的内建命令。
大多数是创建子进程,子进行执行命令,结果再返回给os,shell解释器,再到用户
top,ls,pwd等等命令,shell创建子进程,子进程调用exec*然后执行对应的函数。
在这里插入图片描述

预备知识:
1.用户名,主机名,当前路径,提示符,都是有对应的函数获取对应的内容,但是我们这里为了简便,直接打印显示结果。
如gethostname能获得主机名。最后通过拼接就可以把用户名,主机名,当前路径和提示符都显示出来。getcwd能获取当前的工作目录。

2.strtok会默认把前导空格给忽略,只要我们输入的有效字符。

3.不过我们写的这个shell暂时不支持重定向和管道,再往后的文章后会对这块进行补充。

4.x=100创建本地变量,子进程获取的话是获取不到的。

5.子进程的某些命令不影响父进程从而影响奇异,不如我们用pwd看当前路径,再cd …发现我们路径并没有改变!!!我们通过ls/proc/minishell的进程号当中可以看到cwd当中放着当前的进程的一个路径。
原因:
这是因为当我们cd … 我们期望的更改的是父进程的当前路径,而不是去更改子进程的路径。子进程执行cd …后马上退出了,对父进程不造成影响。
解决方案:让父进程调用系统接口去执行cd,这就是所谓的内置命令,即此时要使用对应的系统接口来完成"命令"的执行。但我们不能把父进程shell的代码替换成cd的代码,不然父进程就无法继续解释其他命令了。
所以我们让系统帮我们完成,我们调用系统接口即可。
在这里插入图片描述

这里我们用的是chdir!!
在这里插入图片描述
在这里插入图片描述
子进程还获得了跟父进程任何打开文件描述符相同的副本,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。
代码:在这里插入图片描述

结果:
在这里插入图片描述

小总结:内置命令就是父进程调用系统调用接口完成的函数,像quit之类的也属于内置命令。



五、PCB的组织方式


引用大佬博客介绍:http://www.cnblogs.com/skywang12345/p/3562146.html
在这里插入图片描述


首先有个预备知识需要铺垫。
1.offsetof

#define offsetof(TYPE, MEMBER) \
 ((size_t) &((TYPE *)0)->MEMBER)

这个是c语言当中常见的宏函数,用途试讲type类型在member当中的位置计算出来。
在这里插入图片描述


2.container_of

#define container_of(ptr, type, member) ({          \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})

首先我们可以通过ptr知道member在type当中的位置,然后我们可以通过offsetof算出member离指向type头指针的位置,再用ptr减去这个偏移量就能找到结构体头的位置。

PCB当中就可以在其中填写一个字段

struct task_struct
{
//struct files_struct;
//struct mm_struct;
struct list_head list;
}

思考
为什么不让这个list_head的前后指针指向PCB呢,这样访问一个进程PCB就直接通过链表结构找就可以,想要访问当前的进程拿到list_head这个字段对应的大结构体,我们直接list_head->prev->next不就拿到了整个PCB结构体了吗?示意图如下:
在这里插入图片描述
答案:
实际上这样子确实可行,但是如果这样子做,链表当中的节点存的就是struct task_struct*,那么我们的list_head如果要管理系统当中的内存管理,驱动管理,文件管理就都需要再设计多一套对应的list_head,即结构体指针指向的类型不是list_head会使这一块设计的更加复杂。

所以linux下的采用的是这样一套逻辑,再通过指针强转的方式就可以赋值给其他的结构体。

linux下进程1.节点的定义:前后指针。

struct list_head {
    struct list_head *next, *prev;
};

2.初始化节点:让节点的前后都指向自己,经典的带头循环双向链表。

#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \
    struct list_head name = LIST_HEAD_INIT(name)

static inline void INIT_LIST_HEAD(struct list_head *list)
{
    list->next = list;
    list->prev = list;
}

3.添加节点:
两种增添方式:list_add插入在头结点的后面。
list_add_tail 插入到尾巴。
注意:一般前带_表示是内部使用,不提供给外部。

static inline void __list_add(struct list_head *new,   struct list_head *prev, struct list_head *next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    prev->next = new;
}

static inline void list_add(struct list_head *new, struct list_head *head)
{
    __list_add(new, head, head->next);
}

static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
    __list_add(new, head->prev, head);
}

在这里插入图片描述

4.删除节点:

list_del删除节点实际上实际上没有释放指向的节点,只是把它从双链表结构拿出来。

list_del_init,则是除了把这个节点从双链表结构拿出来,并且让他自己指向自己。
在这里插入图片描述

static inline void __list_del(struct list_head * prev, struct list_head * next)
{
    next->prev = prev;
    prev->next = next;
}

static inline void list_del(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
}

static inline void __list_del_entry(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
}

static inline void list_del_init(struct list_head *entry)
{
    __list_del_entry(entry);
    INIT_LIST_HEAD(entry);
}

5.替换节点:

旧指针的指向没有改变。

static inline void list_replace(struct list_head *old,
                struct list_head *new)
{
    new->next = old->next;
    new->next->prev = new;
    new->prev = old->prev;
    new->prev->next = new;
}

在这里插入图片描述

6.双链表判空:

static inline int list_empty(const struct list_head *head)
{
    return head->next == head;
}

7.获取节点:
获取节点表示获取大的这个结构体指针。

#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

8.遍历节点:
list_for_each通常用于获取节点,不能用于删除,因为删除会导致找不到当前节点的下一个节点。
list_for_each_safe通常用于删除节点的场景,他用pos指向当前节点,用n指向pos的后继节点,删除节点的时候删除pos就可以。

#define list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)

#define list_for_each_safe(pos, n, head) \
    for (pos = (head)->next, n = pos->next; pos != (head); \
        pos = n, n = pos->next)



最后以一个测试收尾:
双链表list.h如下:

#ifndef _LIST_HEAD_H
#define _LIST_HEAD_H

// 双向链表节点
struct list_head {
    struct list_head *next, *prev;
};

// 初始化节点:设置name节点的前继节点和后继节点都是指向name本身。
#define LIST_HEAD_INIT(name) { &(name), &(name) }

// 定义表头(节点):新建双向链表表头name,并设置name的前继节点和后继节点都是指向name本身。
#define LIST_HEAD(name) \
    struct list_head name = LIST_HEAD_INIT(name)

// 初始化节点:将list节点的前继节点和后继节点都是指向list本身。
static inline void INIT_LIST_HEAD(struct list_head *list)
{
    list->next = list;
    list->prev = list;
}

// 添加节点:将new插入到prev和next之间。
static inline void __list_add(struct list_head *new,
                  struct list_head *prev,
                  struct list_head *next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    prev->next = new;
}

// 添加new节点:将new添加到head之后,是new称为head的后继节点。
static inline void list_add(struct list_head *new, struct list_head *head)
{
    __list_add(new, head, head->next);
}

// 添加new节点:将new添加到head之前,即将new添加到双链表的末尾。
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
    __list_add(new, head->prev, head);
}

// 从双链表中删除entry节点。
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
    next->prev = prev;
    prev->next = next;
}

// 从双链表中删除entry节点。
static inline void list_del(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
}

// 从双链表中删除entry节点。
static inline void __list_del_entry(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
}

// 从双链表中删除entry节点,并将entry节点的前继节点和后继节点都指向entry本身。
static inline void list_del_init(struct list_head *entry)
{
    __list_del_entry(entry);
    INIT_LIST_HEAD(entry);
}

// 用new节点取代old节点
static inline void list_replace(struct list_head *old,
                struct list_head *new)
{
    new->next = old->next;
    new->next->prev = new;
    new->prev = old->prev;
    new->prev->next = new;
}

// 双链表是否为空
static inline int list_empty(const struct list_head *head)
{
    return head->next == head;
}

// 获取"MEMBER成员"在"结构体TYPE"中的位置偏移
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

// 根据"结构体(type)变量"中的"域成员变量(member)的指针(ptr)"来获取指向整个结构体变量的指针
#define container_of(ptr, type, member) ({          \
    const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
    (type *)( (char *)__mptr - offsetof(type,member) );})

// 遍历双向链表
#define list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)

#define list_for_each_safe(pos, n, head) \
    for (pos = (head)->next, n = pos->next; pos != (head); \
        pos = n, n = pos->next)

#define list_entry(ptr, type, member) \
    container_of(ptr, type, member)

#endif

test.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "list.h"

struct person
{
    int age;
    char name[20];
    struct list_head list;
};

void main(int argc, char* argv[])
{
    struct person *pperson;
    struct person person_head;
    struct list_head *pos, *next;
    int i;

    // 初始化双链表的表头
    INIT_LIST_HEAD(&person_head.list);

    // 添加节点
    for (i=0; i<5; i++)
    {
        pperson = (struct person*)malloc(sizeof(struct person));
        pperson->age = (i+1)*10;
        sprintf(pperson->name, "%d", i+1);
        // 将节点链接到链表的末尾
        // 如果想把节点链接到链表的表头后面,则使用 list_add
        list_add_tail(&(pperson->list), &(person_head.list));
    }

    // 遍历链表
    printf("==== 1st iterator d-link ====\n");
    list_for_each(pos, &person_head.list)
    {
        pperson = list_entry(pos, struct person, list);
        printf("name:%-2s, age:%d\n", pperson->name, pperson->age);
    }

    // 删除节点age为20的节点
    printf("==== delete node(age:20) ====\n");
    list_for_each_safe(pos, next, &person_head.list)
    {
        pperson = list_entry(pos, struct person, list);
        if(pperson->age == 20)
        {
            list_del_init(pos);
            free(pperson);
        }
    }

    // 再次遍历链表
    printf("==== 2nd iterator d-link ====\n");
    list_for_each(pos, &person_head.list)
    {
        pperson = list_entry(pos, struct person, list);
        printf("name:%-2s, age:%d\n", pperson->name, pperson->age);
    }

    // 释放资源
    list_for_each_safe(pos, next, &person_head.list)
    {
        pperson = list_entry(pos, struct person, list);
        list_del_init(pos);//删除pos节点
        free(pperson);//free整个结构体
    }

}

运行结果:

==== 1st iterator d-link ====
name:1 , age:10
name:2 , age:20
name:3 , age:30
name:4 , age:40
name:5 , age:50
==== delete node(age:20) ====
==== 2nd iterator d-link ====
name:1 , age:10
name:3 , age:30
name:4 , age:40
name:5 , age:50

参考博客:
https://blog.csdn.net/qq_28992301/article/details/53142826
http://www.cnblogs.com/skywang12345/p/3562146.html

总结

进程的讲解就先告一段落啦,接下来会讲述关于IO,文件管理这一块的知识,如果觉得博主写的还可以的,来一个一键三连吧,祝大家在新的一年好运连连,事事顺心。

这篇关于【Linux】三万字学会进程控制的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!