我们平时写的 C 语言代码,通过编译器编译,最终它会成为一个可执行程序,当这个可执行程序运行起来后(没有结束之前),它就成为了一个进程。 程序是存放在存储介质上的一个可执行文件,而进程是程序执行的过程。进程的状态是变化的,其包括进程的创建、调度和消亡。程序是静态的,进程是动态的。
1 单道程序设计 所有进程一个一个排队执行。若A阻塞,B只能等待,即使CPU处于空闲状态。而在人机交互时阻塞的出现是必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了。
2 多道程序设计 在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证。 在计算机中时钟中断即为多道程序设计模型的理论基础。并发时,任意进程在执行期间都不希望放弃cpu。因此系统需要一种强制让进程让出cpu资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。 操作系统中的中断处理函数,来负责调度程序执行。 在多道程序设计模型中,多个进程轮流使用CPU (分时复用CPU资源)。而当下常见CPU为纳秒级,1秒可以执行大约10亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。 1s = 1000ms 1ms = 1000us 1us = 1000ns 1s = 1000000000ns
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
进程运行时,内核为进程每个进程分配一个PCB(进程控制块),维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
在 /usr/src/linux-headers-xxx/include/linux/sched.h 文件中可以查看struct task_struct 结构体定义
内部成员有很多,我们掌握以下部分即可: 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。 进程的状态,有就绪、运行、挂起、停止等状态。 进程切换时需要保存和恢复的一些CPU寄存器。 描述虚拟地址空间的信息。 描述控制终端的信息。 当前工作目录(Current Working Directory)。 umask掩码。 文件描述符表,包含很多指向file结构体的指针。 和信号相关的信息。 用户id和组id。 会话(Session)和进程组。 进程可以使用的资源上限(Resource Limit)。
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。 在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。 在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞。
①TASKRUNNING:进程正在被CPU执行。当一个进程刚被创建时会处于TASKRUNNABLE,表示己经准备就绪,正等待被调度。 ②TASKINTERRUPTIBLE(可中断):进程正在睡眠(也就是说它被阻塞)等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒,比如给一个TASKINTERRUPTIBLE状态的进程发送SIGKILL信号,这个进程将先被唤醒(进入TASKRUNNABLE状态),然后再响应SIGKILL信号而退出(变为TASKZOMBIE状态),并不会从TASKINTERRUPTIBLE状态直接退出。 ③TASKUNINTERRUPTIBLE(不可中断):处于等待中的进程,待资源满足时被唤醒,但不可以由其它进程通过信号或中断唤醒。由于不接受外来的任何信号,因此无法用kill杀掉这些处于该状态的进程。而TASKUNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程,于是原有的流程就被中断了,这可能使某些设备陷入不可控的状态。处于TASKUNINTERRUPTIBLE状态一般总是非常短暂的,通过ps命令基本上不可能捕捉到。 ④TASKZOMBIE(僵死):表示进程已经结束了,但是其父进程还没有调用wait4或waitpid()来释放进程描述符。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用了wait4(),进程描述符就会被释放。 ⑤TASKSTOPPED(停止):进程停止执行。当进程接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。当接收到SIGCONT信号,会重新回到TASK_RUNNABLE。 如何查看进程状态: ps aux
参数 | 含义 |
---|---|
D | 不可中断 Uninterruptible(usually IO) |
R | 正在运行,或在队列中的进程 |
S | 处于休眠状态 |
T | 停止或被追踪 |
Z | 僵尸进程 |
W | 进入内存交换(从内核2.6开始无效) |
X | 死掉的进程 |
< | 高优先级 |
N | 低优先级 |
s | 包含子进程 |
+ | 位于前台的进程组 |
ps命令可以查看进程信息:
进程是一个具有一定独立功能的程序,它是操作系统动态执行的基本单元。
ps命令可以查看进程的详细状况,常用选项(选项可以不加“-”)如下:
参数 | 含义 |
---|---|
-a | 显示终端上的所有进程,包括其他用户的进程 |
-u | 显示进程的详细状态 |
-x | 显示没有控制终端的进程 |
-w | 显示加宽,以便显示更多的信息 |
-r | 只显示正在运行的进程 |
每个进程都由一个进程号来标识,其类型为 pid_t(整型),进程号的范围:0~32767。进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用。
接下来,再给大家介绍三个不同的进程号。 进程号(PID): 标识进程的一个非负整型数。 父进程号(PPID): 任何进程( 除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。如,A 进程创建了 B 进程,A 的进程号就是 B 进程的父进程号。 进程组号(PGID): 进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID) 。这个过程有点类似于 QQ 群,组相当于 QQ 群,各个进程相当于各个好友,把各个好友都拉入这个 QQ 群里,主要是方便管理,特别是通知某些事时,只要在群里吼一声,所有人都收到,简单粗暴。但是,这个进程组号和 QQ 群号是有点区别的,默认的情况下,当前的进程号会当做当前的进程组号。
pid_t getpid(void);
功能:
获取本进程号(PID)
参数:
无
返回值:
本进程号
pid_t getppid(void);
功能:
获取调用此函数的进程的父进程号(PPID)
参数:
无
返回值:
调用此函数的进程的父进程号(PPID)
pid_t getpgid(pid_t pid);
功能:
获取进程组号(PGID)
参数:
pid:进程号
返回值:
参数为 0 时返回当前进程组号,否则返回参数指定的进程的进程组号
//=============================================================================
// File Name : process_get_pid.c
// Author : FengQQ
//
// Description : 获取进程号
// Annotation :
//
// Created by FengQQ. 2020-9-27
//=============================================================================
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char* argv[])
{
pid_t pid,ppid,pgid;
pid = getpid(); //获取进程号(PID)
printf("pid = %d\r\n",pid);
ppid = getppid(); //获取父进程号(PPID)
printf("ppid = %d\r\n",ppid);
pgid = getpgid(pid); //获取进程群号(PGID)
printf("pgid = %d\r\n",pgid);
return 0;
}
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。
pid_t fork(void);
功能:
用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
参数:
无
返回值:
成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为整型。
失败:返回-1。
失败的两个主要原因是:
1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。
2)系统内存不足,这时 errno 的值被设置为 ENOMEM。
使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。 地址空间: 包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。 子进程所独有的只有它的进程号,计时器等。因此,使用fork函数的代价是很大的。
//=============================================================================
// File Name : process_fork.c
// Author : FengQQ
//
// Description : 创建进程
// Annotation :
//
// Created by FengQQ. 2020-9-27
//=============================================================================
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
pid_t pid;
pid = fork(); //创建进程
if(pid < 0)
{
printf("fork create failed...");
}
else if(pid == 0) //子进程pid返回值=0
{
while(1)
{
printf("I am child process\r\n");
sleep(1);
}
}
else //父进程的pid>0
{
while(1)
{
printf("I am father process\r\n");
sleep(1);
}
}
return 0;
}
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。 父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。 wait() 和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。 注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
pid_t wait(int *status);
功能:
等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
参数:
status : 进程退出时的状态信息。
返回值:
成功:已经结束子进程的进程号
失败: -1
调用 wait() 函数的进程会挂起(阻塞),直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒(相当于继续往下执行)。 若调用进程没有子进程,该函数立即返回;若它的子进程已经结束,该函数同样会立即返回,并且会回收那个早已结束进程的资源。 所以,wait()函数的主要功能为回收已经结束子进程的资源。 如果参数 status 的值不是 NULL,wait() 就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的。 这个退出信息在一个 int 中包含了多个字段,直接使用这个值是没有意义的,我们需要用宏定义取出其中的每个字段。
取出子进程的退出信息 WIFEXITED(status) 如果子进程是正常终止的,取出的字段值非零。 WEXITSTATUS(status) 返回子进程的退出状态,退出状态保存在status变量的8~16位。在用此宏前应先用宏WIFEXITED判断子进程是否正常退出,正常退出才可以使用此宏。 注意:此status是个wait的参数指向的整型变量。
pid_t waitpid(pid_t pid, int *status, int options);
功能:
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
参数:
pid : 参数 pid 的值有以下几种类型:
pid > 0 等待进程 ID 等于 pid 的子进程。
pid = 0 等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。
pid = -1 等待任一子进程,此时 waitpid 和 wait 作用一样。
pid < -1 等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。
status : 进程退出时的状态信息。和 wait() 用法一样。
options : options 提供了一些额外的选项来控制 waitpid()。
0:同 wait(),阻塞父进程,等待子进程退出。
WNOHANG:没有任何已经结束的子进程,则立即返回。
WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(由于涉及到一些跟踪调试方面的知识,加之极少用到)
返回值:
waitpid() 的返回值比 wait() 稍微复杂一些,一共有 3 种情况:
1) 当正常返回的时候,waitpid() 返回收集到的已经回收子进程的进程号;
2) 如果设置了选项 WNOHANG,而调用中 waitpid() 发现没有已退出的子进程可等待,则返回 0;
3) 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在,如:当 pid 所对应的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid() 就会出错返回,这时 errno 被设置为 ECHILD;
进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。 这样就会导致一个问题,如果进程不调用wait() 或 waitpid() 的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
父进程运行结束,但子进程还在运行(未运行结束)的子进程就称为孤儿进程(Orphan Process)。 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。 因此孤儿进程并不会有什么危害。
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。 守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。 Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等。
pid_t vfork(void)
功能:
vfork函数和fork函数一样都是在已有的进程中创建一个新的进程,但它们创建的子进程是有区别的。
返回值:
创建子进程成功,则在子进程中返回0,父进程中返回子进程ID。出错则返回-1。
fork和vfork函数的区别: vfork保证子进程先运行,在它调用exec或exit之后,父进程才可能被调度运行。 vfork和fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不访问该地址空间。相反,在子进程中调用exec或exit之前,它在父进程的地址空间中运行,在exec之后子进程会有自己的进程空间。
//=============================================================================
// File Name : process_vfork.c
// Author : FengQQ
//
// Description : 创建进程
// Annotation :
//
// Created by FengQQ. 2020-9-28
//=============================================================================
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
int a=0;
pid_t pid;
pid = vfork(); //创建进程
if(pid < 0)
{
printf("vfork create failed...\r\n");
}
else if(pid == 0)
{
sleep(5);
execlp("ls","ls",NULL); //子进程exec 或exit之前的代码运行在父进程的地址空间
exit(1); // 退出进程 防止execl执行失败,运行至return
}
else //父进程的pid>0
{
printf("hello vfork...\r\n");// 父进程vfrok之后会等待子进程退出或者调用exec,之后才开始调用运行
while(1);
}
return 0;
}
在 Windows 平台下,我们可以通过双击运行可执行程序,让这个可执行程序成为一个进程;而在 Linux 平台,我们可以通过 ./ 运行,让一个可执行程序成为一个进程。 但是,如果我们本来就运行着一个程序(进程),我们如何在这个进程内部启动一个外部程序,由内核将这个外部程序读入内存,使其执行起来成为一个进程呢?这里我们通过 exec 函数族实现。 exec 函数族,顾名思义,就是一簇函数,在 Linux 中,并不存在 exec() 函数,exec 指的是一组函数,一共有 6 个:
#include
extern char **environ;
int execl(const char *path, const char arg, …/ (char *) NULL */);
int execlp(const char *file,cconst char arg, … / (char *) NULL */);
int execle(const char *path, const char arg, …/, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
六个exec函数中只有execve是真正意义的系统调用(内核提供的接口),其它函数都是在此基础上经过封装的库函数。 l(list): 参数地址列表,以空指针结尾。 参数地址列表 char *arg0, char *arg1, …, char *argn, NULL v(vector): 存有各参数地址的指针数组的地址。 使用时先构造一个指针数组,指针数组存各参数的地址,然后将该指针数组地址作为函数的参数。
p(path) 按PATH环境变量指定的目录搜索可执行文件。 以p结尾的exec函数取文件名做为参数。当指定filename作为参数时,若filename中包含/,则将其视为路径名,并直接到指定的路径中执行程序。 e(environment): 存有环境变量字符串地址的指针数组的地址。execle和execve改变的是exec启动的程序的环境变量(新的环境变量完全由environment指定),其他四个函数启动的程序则使用默认系统环境变量。
exec函数族与一般的函数不同,exec函数族中的函数执行成功后不会返回。只有调用失败了,它们才会返回-1。失败后从原程序的调用点接着往下执行。 在平时的编程中,如果用到了exec函数族,一定要记得加错误判断语句。
exec函数族取代调用进程的数据段、代码段和堆栈段。
一个进程调用exec后,除了进程ID,进程还保留了下列特征不变: 父进程号 进程组号 控制终端 根目录 当前工作目录 进程信号屏蔽集 未处理信号 …
Linux进程间通信之无名管道(三)
链接: link.(https://blog.csdn.net/qq_39721016/article/details/119280670)