1.程序的编译
在完成程序的编写后,并不能直接运行,通常还是需要经历编译器编译的环节,GCC编译器是Linux的默认编译器,大部分Linux发行版中都是默认安装的, 其主要以命令的形式在shell终端中使用。一个C语言程序的编译包括预处理、编译、汇编、链接等环节。
预处理环节对源码进行简单加工,去掉代码中的注释,覆盖原来的#include语句并且把所有的宏定义进行展开,执行所有的条件编译语句,添加必要的调试信息。
编译环节对预处理之后的.i文件进行进一步翻译,在这个过程中,编译器对语法、词法进行分析并优化(具体取决于优化级别,这也是为什么有时候相同的代码在不同的优化级别上的运行结果可能不同的原因),生成对应硬件平台的汇编文件.s。
汇编环节调用汇编器as将汇编文件翻译成可重定位文件,将汇编文件翻译为机器指令并输出.o文件。
汇编后的文件其实就已经是一个ELF文件了,但是其缺乏重定位函数的地址,在链接环节,连接系统提供标准C库和其他的gcc基本库文件,并且将其他的.o文件一起链接,最终得到目标程序,这个参数主要使用-l进行添加, 其中-lc和-lgcc等通常是默认的可以忽略。
2.进程
当源文件通过编译、链接后,会得到一个可执行文件(ELF,executable linkable format),编写的文件与编译完成的程序都是存储在外存(磁盘)中的,它们都是静态的,系统并未为他们分配资源。
当可执行文件得到运行时,操作系统才会把可执行文件中的指令和数据从外存中读取出来并分配系统资源,此时程序从静态变为动态,也就是进程。

进程是操作系统分配资源的基本单位,操作系统以进程为单位分配内存空间、CPU使用权等。进程通常具有以下四个特性:
动态性:进程是程序在处理器上的一个执行过程,其是动态的,在程序运行时被创建出来,当无法获取足够的资源的时候会暂停,当得到资源后会继续执行。
并发性:多个进程同时存储在内存中,其可以与其他程序并发运行,提高资源利用率
独立性:进程是一个独立运行的基本单位,是系统进行资源分配和调度的基本单位,各个进程之间不相干扰
异步性:进程之间以各自独立、不可预知的速度向前推进,一个进程的执行过程、速度是不可预测的,其会受到多方面因素的影响
进程通常由三个部分组成,分别是进程控制块、代码段、数据段。
其中,代码段是进程中能被调度程序调度到CPU上执行的程序代码段,数据段是进程对应的程序原始数据或者程序运行中产生的中间数据等。
在一个操作系统中可能有n个进程同时进行,操作系统会为进程分配一块内存用于记录进程的各项参数信息,这些内存块叫做进程控制块(processing control block,PCB),每个进程都有PCB用于标记进程的存在以及记录进程的信息。
在Linux系统中,PCB是以一个struct task_struct的结构体来存储的,该结构体定义在sched.h头文件中。不同的操作系统对一个进程记录的信息是不同的,其PCB中的内容可能会有一些区别,但基本是大同小异,通常会包含以下内容:
(1)进程标识符:
每个进程都会有一个唯一的PID进程标识符(process identifier)用于区分系统中的进程,这个PID也是Linux内核提供给用户访问进程的一个接口,用户可以通过PID控制进程。
在Windows系统中,可以通过任务管理器查看进程的PID,在Linux系统中提供了shell命令ps来获取进程状态,一般使用ps -ef来产看所有进程及其父子关系或者使用ps -aux查看所有进程以及其资源占用。
(2)进程当前状态
操作系统以进程的状态来作为调度程序分配处理器的依据,进程可以划分为五种基本状态,分别是创建态、就绪态、执行态、阻塞态、结束态。使用ps -aux可以查看当前系统中运行的进程的状态。
创建态:程序正在被创建,但是还没准备好,尚未达到就绪状态,此时向系统申请进程PCB的内存空间,并向PCB中写入进程的相关管理信息,随后系统分配进程运行所需的资源并将进程转换为就绪态。
就绪态:进程已经获取了处理器之外的所有资源,当进程获得处理器资源后可以立即执行。
执行态:进程已经获得了其他资源和处理器资源,正在被CPU运行。
阻塞态:阻塞态也被称为暂停态,指的是进程在执行过程中由于某个时间导致无法继续运行而被暂时停止的状态。阻塞时间结束后,内核调用唤醒函数唤醒进程,此时进程进入就绪态。
结束态:进程正在从系统中消失的过程,其原因可能是正常退出或者问题中断退出,但是存在一种特殊情况,进程退出后其资源还没释放,但是此时进程已经无法被调度和运行,此时程序会进入僵尸态,用户需要对僵尸态程序进程清理。这种情况通常是因为子进程已经结束,父进程没有调用wait()或者waitpid()回收它的退出状态。
3.进程的控制
(1)进程的创建
Linux系统中可以创建若干进程,这些进程还可以创建子进程,一般用进程前驱图来描述创建的进程之间的关系,进程前驱图又名进程树。

Linux系统中运行的所有进程可以构成一个进程数,这些进程树也有一个祖先,可以通过shell命令pstree来打印进程关系。
Linux系统中所有的进程都和一个systemd进程有关系,该进程是是Linux系统创建的第一个进程,全名是system daemon,中文翻译为系统守护进程, 系统中所有的进程都是该进程的子进程。
(2)进程的撤销
进程在完成目标任务后应当及时撤销,及时释放资源。进程撤销分为两种情况,一种是仅撤销指定进程,一种是撤销指定进程及其子孙进程。在Linux系统中提供了一个wait()接口让父进程等待子进程状态改变并且获取已经改变的子进程的信息。
wait()函数的返回值是一个指针,用于记录子进程的退出状态,如果该参数为NULL,则表示当前进程放弃子进程的退出状态。特别注意,wait只会记录一个进程的等待状态,在任意进程被记录后其都将停止阻塞,如果需要等待指定进程,可以使用waitpid等待指定进程结束。在调用wait记录信息后,子进程的相关资源将被回收。
(3)进程的执行
在一个进程中可以创建多个子进程,并且子进程和父进程的代码段是相同的,一份程序会被执行两次,一般情况下是没有必要的。在Linux系统中提供了一组函数接口,可以在进程中加载和执行指定的程序。这组接口以exec为前缀,以后面的字母进行区分,调用这组接口后,当前进程的用户空间代码、数据、堆栈等将替换成新程序,但是进程pid以及父子关系仍然不变。
execl以列表的形式来组织指定程序的参数,execv函数以数组(vector)的形式来组织指定程序的参数,execve在执行指定程序前顺便设置环境变量,execp在执行程序时会自动搜索环境变量PATH的路径。下面编写一个例程进行讲解。
#include <asm-generic/errno-base.h>
#include <errno.h>
#include <sys/errno.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/fcntl.h>
#include <wait.h>
#include <stdio.h>
int main(){
int status;
int status_1;
int status_2;
int status_3;
int status_4;
pid_t pid = fork();
if(pid == 0){
pid_t pid_1 = fork();
if (pid_1 == 0) {
//l:list 以列表的形式组织数据
// 第二个参数:传给新程序的 argv[0](约定俗成写程序名,也可写其他字符串)
execl("/usr/bin/ls", "ls", "-l", "./", NULL);
_exit(NULL);//错误保护,如果exec没有成功,后续程序仍然会执行,此时调用exit进行保护
}
pid_t pid_2 = fork();
if (pid_2 == 0) {
//v:vector 以数组形式组织参数
char *argv[] = {"ls", "-l", "-a", NULL};
execv("/bin/ls", argv);
}
pid_t pid_3 = fork();
if (pid_3 == 0) {
//p:path 自动搜索PATH环境变量找寻命令,可以使用echo $PATH查看环境变量(Linux)
execlp("ls", "ls", "-l", "./", NULL);
_exit(NULL);
}
pid_t pid_4 = fork();
if (pid_4 == 0) {
//e:enviroment 在进程执行前指定自己的环境变量而不是继承父进程的环境变量
char* env[] = {"MYVAR = hello", "PATH = /usr/bin", NULL};
char* argv[] = {"ls", "-l", "a", NULL};
execve("/usr/bin", argv, env);
_exit(NULL);
}
int s[4] = {0};
for (int i = 4; i != 0;) {
if(!s[0]){
s[0] = waitpid(pid_1, &status_1, WNOHANG);
if(s[0] > 0) {
printf("子程序1已执行完毕,资源已清除\n");
i--;
};
if (s[0] < 0) {
if (errno == EINTR) {
s[0] = 0;
}else {
printf("程序1意外退出!\n");
i--;
}
}
}
if(!s[1]){
s[1] = waitpid(pid_2, &status_1, WNOHANG);
if(s[1] > 0) {
printf("子程序2已执行完毕,资源已清除\n");
i--;
};
if (s[1] < 0) {
if (errno == EINTR) {
s[1] = 0;
}else {
printf("程序2意外退出!\n");
i--;
}
}
}
if(!s[2]){
s[2] = waitpid(pid_3, &status_1, WNOHANG);
if(s[2] > 0) {
printf("子程序3已执行完毕,资源已清除\n");
i--;
};
if (s[2] < 0) {
if (errno == EINTR) {
s[2] = 0;
}else {
printf("程序3意外退出!\n");
i--;
}
}
}
if(!s[3]){
s[3] = waitpid(pid_4, &status_1, WNOHANG);
if(s[3] > 0) {
printf("子程序4已执行完毕,资源已清除\n");
i--;
};
if (s[3] < 0) {
if (errno == EINTR) {
s[3] = 0;
}else {
printf("程序4意外退出!\n");
i--;
}
}
}
}
_exit(NULL);
}
waitpid(pid, NULL, 0);
printf("所有程序执行完毕\n");
return 0;
}
事实上,上述函数接口中只有execve是系统调用接口,其他的函数接口都是在这个系统调用接口上进行的封装。此外,Linux系统上还提供了一个system()接口用于执行shell命令。
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://zixblog.com/zix/linuxxitongkaifa/chengxudejibenyuanliyijiduojincheng/