本文主要是介绍Linux系统编程:step1,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一、程序和进程
1、程序和进程
(1)程序:二进制文件,占用的磁盘空间(未运行的,躺在磁盘中)
(2)进程:一个启动的程序(启动之后,程序就和磁盘没有关系了),所有的数据都在内存中,需要占用更多的系统资源(CPU,物理内存),例如:一个剧本(未拍摄,那可以看成程序),要把这个剧本的内容呈现出来(那就是进程),需要演员、道具,场地等(也就是电脑中的系统资源)。需要各个系统资源之间交互。
2、并发和并行
(1)并发:并不是某一个时间点的概念,是一个时间段的概念,常说的高并发服务器,说的是在一短时间内,处理的请求数(例如:1秒内处理的请求个数)。
例如:一个咖啡机(一个CPU),有100个人去打咖啡,1分钟只能打一杯咖啡,我们需要在1分钟之内让所有的人都能取到咖啡,那么采用的算法就是每人有0.6秒的时间取咖啡,虽然没打满,但是取到了咖啡,只有按照同样的方法进行下去,这就是一个高并发的例子。人相当于请求。
cpu在某一时间点只能处理一个进程,为了能并发处理,CPU会把一个时间段切割成一系列的时间碎片,分给不同的进程使用(有点像时分通信),一句话就是切割时间。
(2)并行
上图所示,由一个咖啡机变成了两台,也就是增加了一个CPU,用户看到的是并发量变大了,实际上是增加了处理器。例如:淘宝后台服务器提供服务,是通过许多的服务器进行并行提供服务。
小结:每个进程只能在某一时间段获取CPU,所有的进程都是轮循的获取CPU,一个进程不会独占CPU。
3、pcb
进程控制块(process control block)
作用:存在于内核当中,用来维护进程的相关信息,Linux内核PCB是task_struct结构体,也就是一个类型,里面有需要保存的信息(与C++里面面向对象编程如出一辙)。
重点部分:
- 进程id(系统中每个进程都有一个进程id(相当于每个人的身份证号),C语言中用pid_t(_t看到这个就表示通过了typedef进行了类型定义)类型表示)
- 进程的状态:就绪、运行、挂起、停止和初始状态
- 进程切换需要保存和恢复的一些CPU寄存器(CPU下有很多程序,由于并发执行,所以当一个进程执行到一半的时候,而CPU需要去执行其他进程,那么前一个进程需要保存到寄存器中,保存状态,以用于后面的继续执行)
- 描述虚拟地址空间:虚拟内存通过MMU映射到物理内存
- 描述控制终端的信息:因为运行一个进程之后,需要将信息打印到终端。
- 当前工作目录:在shell进程中,可以通过指令将工作工作目录打印出来
- umask:每一个进程都有这个,
- 文件描述符表
- 和信号相关的信息
- 用户id和组id
- 会话(session)(会话就是多个进程组组成)和进程组(将多个进程放到一起就组成了一个进程组,例如:一个家庭就是进程组,每个成员就是一个进程);也就是进程组成进程组,进程组组成会话(会话相当于一个村,有多个家庭组成)
- 进程可以使用的资源上限(resource limit)
以上信息都是保存在进程控制块(PCB)中的,每启动一个进程,都会在内核中的PCB中有记录。
4、进程的五种状态
初始态:一瞬间的事(怀孕):时间太短,杀死不了(也就是不能直接到终止态)
重要的状态:就绪态、运行态和挂起态。 - 就绪态:CPU中进程时分复用CPU,所以进程之间就是竞争关系,例如:一个进程1运行了,那就要去抢CPU的资源了,等待CPU分配并发时间段,这种等待的状态就是就绪态。(感觉自己准备好了,就缺一个机会)
- 运行态:进程1占用了CPU,可以运行了(人生巅峰,准备好且发挥能力),所以就绪态与运行态区别就是有无CPU。
- 挂起态:sleep()函数就是让进程睡几秒钟,当睡醒了之后就条件满足可以继续执行。但是,睡醒了不能直接去进入运行态,还要去就绪态去抢CPU,不能插队,睡了一觉,就要老老实实去排队。
终止态:kill -9 pid
二、进程控制(关键)
进程控制:讲的就是如何建立进程
程序a.out 编程进程,去shell进程中执行 ./a.out 变为子进程
Q:如何让一个父进程创建一个子进程?
A:int fork(void)
fork(叉子),子进程相当于父进程的一个拷贝,也就是子进程和父进程地址空间一样,也就是上图中除内核的其他部分完全一样。也就是用户区一样。但是内核区不一样,因为内核区中保存了PCB,而PCB中中保存的进程id是不一样的,所以,子、父进程不同的就是进程id不一样。
fork不同之处:有两个返回值,一个进程变为两个进程之后,因为拷贝,两个进程的地址空间是一样的,数据完全一样,在代码段.text中,父子进程都有一个fork,所以都有一个返回值,父进程就返回子进程的一个id,子进程就返回0,所以就有了两个返回值。
上图所示,父进程中fork之后,其实就拷贝了拥有相同代码的子进程(右边),但是是在父进程中fork的所以pid得到的是子进程的id,而在子进程中,由于没有fork,所以返回的pid是0,以此区分子进程和父进程。也就是返回值是由不同的进程return出来的,拷贝之后就有两个return了。
问题:
Q1:fork函数的返回值 A1:两个,pid>0 父进程的返回值,pid == 0, 子进程的返回值
Q2:子进程创建成功后,代码的执行位置?
A2:正如上面所示,子进程从非灰色部分开始执行,而父进程从头到尾执行。父进程执行到了那,子进程就从那开始执行。
Q3:父子进程的执行顺序?
A3:不一定,谁抢到CPU,谁执行。
Q4:如何区分父子进程?
A4:通过fork的返回值。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
int main(int argc,const char* argv[])
{pid_t pid;for(int i=0;i<4;i++){printf("---------------i=%d\n",i);}pid = fork();if(pid >0){printf("parent process,pid=%d\n",getpid());}else if(pid ==0){printf("child process,pid=%d\n",getpid());}for(int i=0;i<4;i++){printf("---------------i=%d\n",i);}return 0;
}
有上面实验的结果可知,子进程中是从fork下面的开始开始执行的。所以子进程虽然有代码段,但是不会执行。让父进程睡一会(父进程结束的太快了,已经结束了,使得子进程将init进程作为父进程了)
出现上面的原因:
2、ps和kill命令
Q:如何获取当前进程的id?
A:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
int main(int argc, const char* argv[])
{
while(1)
{
printf(“hello,world\n”);
sleep(1);
}
return 0;
}
`
TTY :就是终端,au显示依赖终端的选项。
Q:终端对于进程来说有什么用呢?
A:没有终端就看不到输出,所以依赖终端的进程是需要和用户交互的。
|:表示管道。
kill:发信号给某个进程
只需学1-31个信号即可。
杀死某个进程:kill -9 (仅为9号信号为SIGKILL表示杀死信号)
3、进程间数据共享
关于fork,刚fork之后,两个地址空间用户区数据完全相同,后续各自都做了各自不同的操作,各个进程的地址空间中的数据是完全独立的,如下图所示,n=24子、父一样的,在子父进程各自对n进行操作时,是互不影响的,一个++,一个–;扩展:研究这个n = 24,但对于这个n只是进行读操作时,那么在物理内存中n是一份,也就是子父进程只是读这个n,而不改变,这样可以节省内存,也就是读时共享n;但是在进行读写操作时,因为可以子父进程可以修改它,为了子父进程互不影响,子进程就拷贝一份,也就是不是共享的。
上图所示,在父或子进程对变量进行写操作时,都会在物理内存开辟一块内存保存修改的值,而且父子变量内存不共享。
Q:父、子进程之间,是否可以通过一个全局变量进行通信?
A:不能,因为根据上面的原理,写复制,父子各自的变量物理内存不一样,所以内存不能共享。
小结:读时共享,写时复制
扩展:需要实现共享就牵涉到进程间的通信。
4、exec函数族
(1)作用
①让父子进程执行不相干的操作
②父子进程代码段一样的,而exec函数组可以替换父子空间的源代码,也就是.text段
注意:替代.text中代码的是可执行文件源代码,因为是进程,所以需要是可执行的程序。其中的ls是一个例子,它是shell中的一个可执行文件,所以当替换只有,进程就相当于执行命令ls了。鸠占鹊巢的感觉。
(2)使用
例子:
“who can arrive here”,只执行了一次,并且是在父进程3047中,而子进程3048执行的程序是ls,所以得出结论,当子进程使用execl()时,子进程中的.text中的源代码全部被execl程序中的代码所替换,也就不能执行if(pid==0)外的内容了。
5、进程回收
(1)孤儿进程
①爹生孩子
②爹先死,孩子还活着,孩子就叫做孤儿进程
③孤儿被init进程领养,init进程变为孤儿进程的父亲。(这就是为什么写程序会出现子进程的父进程变为1,也就是原来的父进程执行的太快了,以至于原来父进程结束了,而子进程还没执行完,就成孤儿了。)
④为了释放子进程占用的系统资源
进程结束之后,能够释放用户区空间;释放不了PCB,必须由父进程释放(key)。
这里就出现了孤儿进程被ppid=1的进程领养了。
(2)僵尸进程
①孩子死了,爹还活着,爹不去释放子进程的pcb,孩子就变成 了僵尸进程。(因为只能由父进程释放)
②不是活着的进程
z:表示zonbie僵尸的意思,defunct表示a.out进程为僵尸进程。
如何杀死僵尸进程呢?通过杀死他爹的进程就行了,也就是3516,直接杀死僵尸进程是不能消除的,相当于鞭尸。
(3)进程回收
①wait-阻塞函数(与socket差不多,也就是一直阻塞在哪,等待消息)
pid_t wait(int * status);
作用:因为子进程需要父进程进行回收,而且在并发运行的时候,父子进程的执行顺序是不一定的,所以需要wait函数去使得父进程等待子进程结束,然后回收,最后父进程结束,造成有顺序的执行。
如上died child pid是最后执行的,表明父进程是在子进程执行完,回收它之后执行完的。
②
这里最后的返回值为9,表明父进程一直执行到最后,所以返回了9,也就是 正常退出
当子进程一直循环不退出时,父进程将一直等待:
有两个进程在运行,kill -9 4051 。通过信号杀死子进程,预期不会返回12,会返回9
终止了。
小结:
父进程结束的比子进程早,则会造成子进程成为孤儿进程;
父进程结束的比子进程晚,并且父进程一直不退出,则会造成子进程成为僵尸进程;(所以消灭僵尸进程是通过kill父进程达到目的)
解决方法:通过父进程中调用wait函数。
这篇关于Linux系统编程:step1的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!