Linux系统编程:step1
一、程序和进程
1、程序和进程
(1)程序:二进制文件,占用的磁盘空间(未运行的,躺在磁盘中)
(2)进程:一个启动的程序(启动之后,程序就和磁盘没有关系了),所有的数据都在内存中,需要占用更多的系统资源(CPU,物理内存),例如:一个剧本(未拍摄,那可以看成程序),要把这个剧本的内容呈现出来(那就是进程),需要演员、道具,场地等(也就是电脑中的系统资源)。需要各个系统资源之间交互。
2、并发和并行
(1)并发:并不是某一个时间点的概念,是一个时间段的概念,常说的高并发服务器,说的是在一短时间内,处理的请求数(例如:1秒内处理的请求个数)。
例如:一个咖啡机(一个CPU),有100个人去打咖啡,1分钟只能打一杯咖啡,我们需要在1分钟之内让所有的人都能取到咖啡,那么采用的算法就是每人有0.6秒的时间取咖啡,虽然没打满,但是取到了咖啡,只有按照同样的方法进行下去,这就是一个高并发的例子。人相当于请求。
cpu在某一时间点只能处理一个进程,为了能并发处理,CPU会把一个时间段切割成一系列的时间碎片,分给不同的进程使用(有点像时分通信),一句话就是切割时间。
(2)并行
上图所示,由一个咖啡机变成了两台,也就是增加了一个CPU,用户看到的是并发量变大了,实际上是增加了处理器。例如:淘宝后台服务器提供服务,是通过许多的服务器进行并行提供服务。
小结:每个进程只能在某一时间段获取CPU,所有的进程都是轮循的获取CPU,一个进程不会独占CPU。
3、pcb
进程控制块( block)
作用:存在于内核当中,用来维护进程的相关信息,Linux内核PCB是结构体,也就是一个类型,里面有需要保存的信息(与C++里面面向对象编程如出一辙)。
重点部分:
二、进程控制(关键)
进程控制:讲的就是如何建立进程
程序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,以此区分子进程和父进程。也就是返回值是由不同的进程出来的,拷贝之后就有两个了。
问题:
Q1:fork函数的返回值 A1:两个,pid>0 父进程的返回值,pid == 0, 子进程的返回值
Q2:子进程创建成功后,代码的执行位置?
A2:正如上面所示,子进程从非灰色部分开始执行,而父进程从头到尾执行。父进程执行到了那,子进程就从那开始执行。
Q3:父子进程的执行顺序?
A3:不一定,谁抢到CPU,谁执行。
Q4:如何区分父子进程?
A4:通过fork的返回值。
#include
#include
#include
#include
#include
#include
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:
#
#
#
#
#
#
int main(int argc, const char* argv[])
while(1)
(“hello,world\n”);
sleep(1);
0;
TTY :就是终端,au显示依赖终端的选项。
Q:终端对于进程来说有什么用呢?
A:没有终端就看不到输出,所以依赖终端的进程是需要和用户交互的。
|:表示管道。
kill:发信号给某个进程
只需学1-31个信号即可。
杀死某个进程:kill -9 (仅为9号信号为表示杀死信号)
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 here”,只执行了一次,并且是在父进程3047中,而子进程3048执行的程序是ls,所以得出结论,当子进程使用execl()时,子进程中的.text中的源代码全部被execl程序中的代码所替换,也就不能执行if(pid==0)外的内容了。
5、进程回收
(1)孤儿进程
①爹生孩子
②爹先死,孩子还活着,孩子就叫做孤儿进程
③孤儿被init进程领养,init进程变为孤儿进程的父亲。(这就是为什么写程序会出现子进程的父进程变为1,也就是原来的父进程执行的太快了,以至于原来父进程结束了,而子进程还没执行完,就成孤儿了。)
④为了释放子进程占用的系统资源
进程结束之后,能够释放用户区空间;释放不了PCB,必须由父进程释放(key)。
这里就出现了孤儿进程被ppid=1的进程领养了。
(2)僵尸进程
①孩子死了,爹还活着,爹不去释放子进程的pcb,孩子就变成 了僵尸进程。(因为只能由父进程释放)
②不是活着的进程
z:表示僵尸的意思,表示a.out进程为僵尸进程。
如何杀死僵尸进程呢?通过杀死他爹的进程就行了,也就是3516,直接杀死僵尸进程是不能消除的,相当于鞭尸。
(3)进程回收
①wait-阻塞函数(与差不多,也就是一直阻塞在哪,等待消息)
pid_t wait(int * );
作用:因为子进程需要父进程进行回收,而且在并发运行的时候,父子进程的执行顺序是不一定的,所以需要wait函数去使得父进程等待子进程结束,然后回收,最后父进程结束,造成有顺序的执行。
如上died child pid是最后执行的,表明父进程是在子进程执行完,回收它之后执行完的。
②
这里最后的返回值为9,表明父进程一直执行到最后,所以返回了9,也就是 正常退出
当子进程一直循环不退出时,父进程将一直等待:
有两个进程在运行,kill -9 4051 。通过信号杀死子进程,预期不会返回12,会返回9
终止了。
小结:
父进程结束的比子进程早,则会造成子进程成为孤儿进程;
父进程结束的比子进程晚,并且父进程一直不退出,则会造成子进程成为僵尸进程;(所以消灭僵尸进程是通过kill父进程达到目的)
解决方法:通过父进程中调用wait函数。