Linux 进程
一、进程的概念
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体.
进程的定义
狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程的概念主要有两点:
第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。
线程的概念
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。
一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。
另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
进程与线程的关系
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
- 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
- 线程的划分尺度小于进程,使得多线程程序的并发性高。
- 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
- 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
优缺点:
线程和进程在使用上各有优缺点:
线程执行开销小,但不利于资源的管理和保护,而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。
二、进程的相关属性
进程ID
每一个进程都有一个唯一的标识符,进程ID 简称pid
- 进程id 一般默认的最大值为32768,不过也是可以修改的,当然一般情
况下不需要这么做。如果当前进程是1000,那么下一个分配的进程就是
1001,它是严格线性分配的。 - 除了init 进程,其它进程都是由其它进程创立的。创立新进程的进程叫父
进程,新进程叫子进程。 - man 2 getpid 获取子进程(当前进程)的ID
- man 2 getppid 获取父进程的ID
进程相关命令
ps:只显示当前终端的进程状态
ps -e:查看前后台进程的状态
ps -ef:显示更为详细的进程信息
ps aux:查看所有进程
ps aux | grep(进程名):查看指定进程
getpid:获取子进程ID(bash)
getppid:获取父进程ID
ps -l:详细的查看当前终端的进程信息
x86-Ubuntu以及开发板上执行top命令
进程的状态
待补充
“R (running)”——运行状态(并不意味进程一定在运行中,表明进程在运行中或在运行队列中)
“S (sleeping)”——睡眠状态(做事情,进程在等待事件完成,可以自己或其他程序唤醒也叫做可中断睡眠)
“D (disk sleep)”——磁盘睡眠状态(不会因为任何命令/指令终止,保证数据完整性,只能关机重启,也叫不可中断睡眠)
“T (stopped)”——暂停状态(什么事情也不做)
“t (tracing stop)”——
“X (dead)”——死亡状态
“Z (zombie)”——僵尸状态
进程的优先级
待补充
topeet@ubuntu:/var/tftpboot$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 3071 2257 0 80 0 - 6994 wait pts/1 00:00:00 bash
0 R 1000 3372 3071 0 80 0 - 3482 - pts/1 00:00:00 ps
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
NI:就是我们所要说的nice值,其表示进程可被执行的优先级的修正数值。
如前面所说,PRI值越小越快被执行,那么加入nice值后,使得PRI变为:PRI(new)=PRI(old)+nice。
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。
到目前为止,更需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念, 但是进程nice值会影响到进程的优先级变化。
修改进程优先级的命令主要有两个:nice,renice
1,nice
nice -n 数值 +文件名
nice的范围为-20 ~ 19
PRI的范围为60 ~ 99
2,renice(进程跑起来,动态调整)
renice 数值 -p 文件名
renice改变的值是从最开始的数值上改变
./myenv &(把可执行程序myenv放在后台运行)
三、exec函数族
说明
fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。
在Linux中使用exec函数族主要有以下两种情况:
a. 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何 exec 函数族让自己重生。
b. 如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生。
exec函数族语法
实际上,在Linux中并没有exec函数,而是有6个以exec开头的函数族,下表列举了exec函数族的6个成员函数的语法。
exec函数族参数:
– “l”和“v”表示参数是以列表还是以数组的方式提供的。
– “p”表示这个函数的第一个参数是*path,就是以绝对路径来提供程序的路径,也可以以当前目录作为目标。
– “e”表示为程序提供新的环境变量
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., 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[]);
具体的用法可看最后的实例
执行的流程图
graph TD
A[运行test1的main函数] -->B(exec函数族)
B --> C{调用test2成功?}
C --> |NO| D[END]
C --> |Yes| E[程序test2开始运行]
E --> F[执行test2的main函数]
F --> G(END)
四、进程创建
进程创建函数fork()
man 2 fork
#include <unistd.h>
pid_t fork(void);
返回值:
成功:子进程返回0,父进程返回子进程id,
出错:返回-1给父进程。
进程创建的一般过程
- 给新建的进程分配一个内部的标识符,在内核中分配PCB(进程描述符)。
- 复制父进程的环境
- 为进程分配资源(代码,数据,堆栈)
- 父进程地址空间的内容也复制到新的进程空间中
- 将该进程放到就绪队列中
- 向父进程返回子进程的进程号,对子进程返回0
- 当一个进程fork出一个子进程后,就有两个二进制代码相同的进程,并且运行到相同的地方,但每个进程都将开始执行自己的代码。
fork()调用失败的原因
- 系统中进程太多了
- 或实际用户的进程数超过了限制(每台电脑都有一个进程的最大数)
- 系统不支持fork()函数,(如windows)
父子进程地址
父子进程代码段是共享的,父子对应的都看为只读的,这样代码段数据段都为共享,若数据段要进行修改,则进行写时拷贝,不占用磁盘内存,为虚拟的。
父子进程会出现相同的地址,不同的值。虚拟地址(地址空间上的地址)一样,物理地址不一样。
创建的pcb以父进程为模版,代码、共享数据各自有一份(利用了写时拷贝)。
也就是说,fork创建的子进程,与父进程的地址相同但是内容不同。
那么它是怎么做到的呢?
虚拟地址 与 物理地址的转换过程是有 mmu 决定的,代码在物理地址中存储,
通过页表和mmu映射到同一物理内存就可以共享,映射到不同的虚拟地址中就数据各一份。
环境变量:
环境变量具有全局属性:被子进程继承,当前进程以及子进程都可以被继承。
局部变量:只有本shell/进程内部存在。
五、实例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
char *arg[] = {"ls","-a",NULL};
if(fork() == 0){
//in child1
printf("fork1 is OK;execl\n");
if(execl("/bin/ls","ls","-a",NULL) == -1){
perror("execl error");
exit(1);
}
}
usleep(20000);
if(fork() == 0){
//in child2
printf("fork2 is OK;execv\n");
if(execv("/bin/ls",arg) == -1){
perror("execv error");
exit(1);
}
}
usleep(20000);
if(fork() == 0){
//in child3
printf("fork3 is OK;execlp\n");
if(execlp("ls","ls","-a",NULL) == -1){
perror("execlp error");
exit(1);
}
}
usleep(20000);
if(fork() == 0){
//in child4
printf("fork4 is OK;execvp\n");
if(execvp("ls",arg) == -1){
perror("execvp error");
exit(1);
}
}
usleep(20000);
if(fork() == 0){
//in child5
printf("fork5 is OK;execle\n");
if(execle("/bin/ls","ls","-a",NULL,NULL) == -1){
perror("execle error");
exit(1);
}
}
usleep(20000);
if(fork() == 0){
//in child6
printf("fork6 is OK;execve\n");
if(execve("/bin/ls",arg,NULL) == -1){
perror("execve error");
exit(1);
}
}
//加入小延时可以避免发生混乱的情况
usleep(20000);
return 0;
}