在开始介绍进程之前,我们先来看下面这张照片,这是我们在Windows系统下经常会遇到的情况,有时候遇到这种情况,真想砸电脑(太不给力了,特别是在打游戏起劲的时候,你说来了这样一个大招,这谁顶得住):
哈哈哈,开玩笑的,一般这种情况都是电脑配置稍微低了一点,然后打开了太多应用,才导致的,打游戏那必须高配置啊!!!好了,我们开始进入今天的主题分享。
一、主角main()函数:
1、说到main函数,这个是写程序的大门啊,几乎每个程序都有这个(不管是c语言,还是面向过程语言java、c++、c#等,main函数都是主角呢!),记得刚开始学编程的时候,那时候就是从这里开始萌芽的。那么写了这么多函数,为啥要从这里开始“进门”呢,个人理解就是一种规定。其实程序一开始执行前,它不是立马执行main()函数里面的内容的;记得学stm32的时候,里面在讲解启动文件的时候,也是这个原理,它不是立马执行main()函数里面的内容的,而且要准备一些前期工作后,最后才来到main()函数:
然而在我们Linux系统里面编程,它也是要准备一些前期工作的:它要有编译链接时的引导代码-------操作系统下的应用程序其实在main执行前也需要先执行一段引导代码才能去执行main,我们写应用程序时不用考虑引导代码的问题,编译连接时(准确说是连接时)由链接器将编译器中事先准备好的引导代码给连接进去和我们的应用程序一起构成最终的可执行程序。最后当我们去执行一个程序时(譬如./a.out,譬如代码中用exec族函数(它就是提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。)来运行)加载器负责将这个程序加载到内存中去执行这个程序。所以说,程序在编译连接时用链接器,运行时用加载器,这两个东西对程序运行原理非常重要。下面是查看它怎样链接形成的过程:
2、上面说的是程序的开始,那么程序的结束是怎样来终止的呢?学过c语言的都知道,在结束main()函数时,都会程序末尾加一个return 0来表示程序的结束,然而其实还有两个函数可以来结束一个程序-------exit,_exit。当然这里的终止程序是正常的终止,有的时候程序运行过程中会遇到一些bug,也就会出现一些不正常的提前来结束程序,也不能达到你要的效果了。
3、使用atexit注册进程终止处理函数,我们使用man手册来查看它的介绍:
int atexit(void(*func)(void));
注意:atexit()注册的函数类型应为不接受任何参数的void函数,atexit的参数是一个函数地址(或者说是一个函数指针),当调用此函数(指的是atexit的参数 )时无须传递任何参数,该函数也不能返回值,atexit函数称为终止处理程序注册程序,注册完成以后。按照ISO C的规定,一个进程可以登记多达32个函数,这些函数将由exit自动调用。exit调用这些注册函数的顺序与它们 登记时候的顺序相反(压栈过程,就是先进后出)。同一个函数如若登记多次,则也会被调用多次。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void func1(void)
{
printf("func1\n");
}
void func2(void)
{
printf("func2\n");
}
int main(void)
{
printf("hello world.\n");
// 当进程被正常终止时,系统会自动调用这里注册的func1执行
atexit(func2);
atexit(func1);
return 0;
}
注:这里还有一点要注意的地方就是return、exit和_exit的区别:return和exit效果一样,都是会执行进程终止处理函数,但是用_exit终止进程时并不执行atexit注册的进程终止处理函数(类似于单片机的中断)。下面我们来演示一下_exit来终止程序的效果:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void func1(void)
{
printf("func1\n");
}
void func2(void)
{
printf("func2\n");
}
int main(void)
{
printf("hello world.\n");
// 当进程被正常终止时,系统会自动调用这里注册的func1执行
atexit(func2);
atexit(func1);
printf("i like the rtos\n");
_exit (0);
}
二、什么是进程?
1、前面的索引,都是为了现在来讲什么是进程?进程是一个独立的可调度的任务,进程是一个抽象实体。当系统在执行某个程序时,分配和释放的各种资源,进程是一个程序的一次执行的过程(通俗的讲,进程就是程序的一次运行过程,一个静态的可执行程序a.out的一次运行过程(./a.out去运行到结束)就是一个进程。更加好理解就是我文章开头的那张照片里面显示那样,其实简单来理解,进程就我们在电脑上运行的一个应用软件。同时,进程还有一个概念,进程控制块PCB(process control block),内核中专门用来管理一个进程的数据结构。)。这里要注意进程和程序的区别:
------程序是静态的,它是一些保存在磁盘上的指令的有序集合,没有任何执行的概念。
------进程是一个动态的概念,它是程序执行的过程,包括创建、调度和消亡 进程是程序执行和资源管理的最小单位 。
2、进程ID(就是进程号,类似于我们之前的文件描述符),当进程创建或者开启的时候,系统会随机分配一个非负整数用于标识当前进程,称之为进程号。ID为0的进程是调度进程,即交换进程;该进程是内核的一部分,即系统进程,所有子进程的父ID不可能是0,而init进程1是所有孤儿进程的父进程,它由内核调用,但不属于内核,一般做一些初始化的工作。进程ID2是页守护进程,此进程负责支持虚拟存储系统的分页操作。在我们Linux系统有一些函数可以获得进程号:getpid(获得当前进程的ID)、getppid(获得父进程ID)、getuid(获取当前进程的用户ID,比如root用户或是普通用户)、geteuid(获得当前有效用户进ID)、getgid(获得当前进程的组ID)、getegid(获得有效用户组进ID),具体用法可以用man 手册来查看,这里我就不一一举例了,这个比较简单的。
注意:实际用户ID(RUID):用于标识一个系统中用户是谁,一般是在登录之后,就被唯一确定的,就是登陆的用户的uid。而有效用户ID(EUID):用于系统决定用户对系统资源的权限。也就是说当用户做任何一个操作时,最终看它有没有权限,都是在判断有效用户ID是否有权限,如果有,则OK,否则报错不能执行。在正常情况下,一个用户登录之后(我们假设是A用户),A用户的有效用户ID和实际用户ID是相同的,但是如果A用户在某些场景中想要执行一些特权操作,而上面我们说到用户的任何操作,LINUX内核都是通过检验有效用户ID来判断当前执行这个操作的用户是否具有权限,显然是特权操作,A用户没有权限,所以A用户就只能通过一定的手段来修改当前的有效用户ID使其具有执行特权操作的权限。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t p1 = -1, p2 = -1;
printf("hello.\n");
p1 = getpid();
printf("pid = %d.\n", p1);
p2 = getppid();
printf("parent id = %d.\n", p2);
return 0;
}
3、进程类型:
a、交互进程:该类进程是由shell控制和运行的。交互进程既可以在前台运行,也可以在后台运行
b、批处理进程:该类进程不属于某个终端,它被提交到一个队列中以便顺序执行。
c、守护进程:该类进程在后台运行。它一般在Linux启动时开始执行,系统关闭时才结束。
三、fork()函数来创建子进程实战: 1、首先,我们要明白为什么要来创建子进程-------每一次程序的运行都需要一个进程;多进程实现宏观上的并行(类似于rtos里面的任务调度器来分配任务的执行)。其实操作系统每次重新创建一个进程都是需要一定成本的,因为对于PCB这个结构体块来说需要占有一定的内存;如果完全建立一个全新的进程出来是需要占用很多资源的,比如时间资源;但是从一个老进程那里直接copy出一个新进程,并且在这个新进程中进行更改某些模块,会节约很多资源,效率也会高很多。这就是建立一个新的进程的主要意义。
2、fork的内部创建进程的原理:
进程的分裂生长模式(类似生物里面的细胞分裂生长)。如果操作系统需要一个新进程来运行一个程序,那么操作系统会用一个现有的进程来复制生成一个新进程。老进程叫父进程,复制生成的新进程叫子进程。fork()函数调用一次会返回2次,返回值等于0的就是子进程,而返回值大于0的就是父进程。我们先来用man 手册来具体查看它的用法:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t p1 = -1;
p1 = fork(); // 返回2次
if (p1 == 0)
{
// 这里一定是子进程
// 先sleep一下让父进程先运行,先死
sleep(1);
printf("子进程, pid = %d.\n", getpid());
printf("hello world.\n");
printf("子进程, 父进程ID = %d.\n", getppid());
}
if (p1 > 0)
{
// 这里一定是父进程
printf("父进程, pid = %d.\n", getpid());
printf("父进程, p1 = %d.\n", p1);
}
if (p1 < 0)
{
// 这里一定是fork出错了
}
// 在这里所做的操作
//printf("hello world, pid = %d.\n", getpid());
return 0;
}
四、总结:
今天进程的学习分享就结束了,后面还会有更加深入的对进程的学习。所有的源代码可以到我的github里面获取:https://github.com/1121518wo/linux-/tree/master