目录
进程组PGID:对相同类型的进程进行管理,由首进程PID决定
在Linux中是通过“进程表”来记录与进程相关的信息的。进程表是一个数据结构,把当前加载在内存中的所有进程的有关信息保存在一个表中,其中包括进程的PID、进程的状态、命令字符串和其它一些ps命令输出(如pstree可查看进程间的关系)的各类信息。
PID是一个16位的正整数,默认取值范围是2~32768(可以修改),由Linux在启动新进程时自动依次分配(当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的PID,当PID的数值达到最大时,系统将重新选择下一个未使用的数值,新的PID重新从2开始,因为PID数值为1是为特殊进城init保留)。init进程是系统在运行时就存在的第一个进程,负责管理其他进程。
任何进程(子进程,除init进程)都是由另一个进程(父进程)启动,父进程号无法在用户层修改。
程序并不能单独执行,只有将程序加载到内存中,系统为他分配资源后才能够执行,这种执行的程序称为进程。即进程是系统进行资源分配和调度的一个独立单位,每个进程都有自己单独的地址空间。
在Linux系统中,程序只是静态的文件,而进程是一个动态的实体。比如运行一个可执行文件,通常在Shell中输入命令运行就可以了,其内部整体转换过程如下:
查找命令对应程序文件的位置。
使用fork()函数启动一个新进程。
在新进程中调用exec族函数装载程序文件,并执行程序文件的main()函数。
终端查询进程参数
ps参数 | 描述 |
-e | 列出所有进程 |
-f | 显示进程的全部信息 |
-h | 不显示进程标题列出所有进程 |
-l | 长格式显示进程 |
-w | 宽格式显示进程 |
-a | 显示终端上的所有进程,包括其他用户的进程 |
-r | 只显示正在运行的进程 |
-x | 显示没有控制终端的进程 |
-u | 显示进程的归属用户及内存使用情况 |
-j | 显示进程归属的进程组id、会话id、父进程id |
ps auf(关注进程本身):用户名 PID 占用CPU% 占用内存% 虚拟内存大小 物理内存大小 进程关联的终端 进程当前状态 进程启动时间 进程运行时间 进程执行的程序。
ps axjf(关注进程间的关系):PPID PID PGID SID 进程关联的终端 是否是守护进程(-1是) 进程当前状态 用户id 进程运行时间 进程执行的程序。
进程的三态模型(至少)
运行态:进程占用处理器正在运行的状态。进程已获得CPU占有权,其程序正在执行。在单处理器系统中,只有一个进程处于执行状态;在多处理系统中,则有多个进程处于执行状态。
就绪态:当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU占有权,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,存放于就绪队列。
等待态(阻塞态、睡眠态):一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,此时即使把处理器分配给进程也无法运行。
运行态→等待态:等待使用资源,如等待外设传输、等待人工干预。
等待态→就绪态:资源得到满足,如外设传输完成、人工干预完成。
运行态→就绪态:运行时间片到或出现更高优先级进程等。
就绪态→运行态:CPU空闲时从就绪队列中选择最先出来的队列。
进程的五态模型
创建态:进程被创建并尚未进入就绪队列的状态。
创建一个进程需要两个步骤:为新进程分配所需要的资源和建立必要的管理信息;设置该进程为就绪态,并等待被调度执行。
终止态:指进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。处于终止态的进程不再被调度执行,下一步将被系统撤销,最终从系统中消失。
终止一个进程需要两个步骤:先对操作系统或相关的进程进行善后处理(如抽取信息);然后回收占用的资源并被系统删除。
NULL→创建态:执行一个程序,创建一个子进程。
创建态→就绪态:当操作系统完成了进程创建的必要操作,并且当前系统的性能和虚拟内存的容量均允许。
就绪态→运行态:CPU空闲时从就绪队列中选择最先出来的队列。
运行态→就绪态:运行时间片到或出现更高优先级进程等。
运行态→终止态:当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结。
运行态→等待态:等待使用资源,如等待外设传输、等待人工干预。
等待态→就绪态:资源得到满足,如外设传输完成、人工干预完成。
就绪态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
等待态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
终止态→NULL:完成善后操作。
进程的七态模型
三态模型和五态模型都是假设所有进程都在内存中有序不断的创建进程,当系统资源(尤其是内存资源)已经不能满足进程运行的要求时,必须把某些进程挂起,对换到磁盘对换区中,释放它占有的某些资源,暂时不参与低级调度。起到平滑系统操作负荷的目的。
引起进程挂起的原因主要有:
1.终端用户的请求,使正在执行的程序暂停执行。(挂起)
当终端用户在自己的程序运行期间发现有可疑问题时,希望暂停使自己的程序静止下来。若此时用户进程正处于就绪状态而未执行,则该进程暂不接受调度,以便用户研究其执行情况或对程序进行修改。
2.父进程的请求。
有时父进程希望挂起自己的某个子进程,以便考察和修改子进程,或者协调各子进程间的活动。
3.负荷调节的需要。
当实时系统中的工作负荷较重,已可能影响到对实时任务的控制时,可由系统把一些不重要的进程挂起,以保证系统能正常运行。
4.操作系统的需要。
操作系统有时希望挂起某些进程,一遍检查运行中的资源使用情况或进行记账。
5.对换的需要。
为了缓和内存紧张的情况,将内存中处于阻塞状态的进程换至外存上。
挂起就绪态:进程具备运行条件,但目前在外存中,只有它被对换到内存才能被调度执行。
挂起等待态:表明进程正在等待某一个事件发生且在外存中。
NULL→创建态:执行一个程序,创建一个子进程。
创建态→就绪态:当操作系统完成了进程创建的必要操作,并且当前系统的性能和虚拟内存的容量均允许。
就绪态→运行态:CPU空闲时从就绪队列中选择最先出来的队列。
运行态→就绪态:运行时间片到或出现更高优先级进程等。
运行态→终止态:当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结。
运行态→等待态:等待使用资源,如等待外设传输、等待人工干预。
等待态→就绪态:资源得到满足,如外设传输完成、人工干预完成。
等待态→挂起等待态:操作系统根据当前资源状态和性能要求,可以决定把等待态进程对换到外村中成为挂起等待态。
挂起等待态→等待态:当一个进程等待一个事件时,原则上不需要把它调入内存中。但是在下面一种情况下,这一状态变化是可能的。当一个进程退出后,主存已经有了一大块自由空间,而某个挂起等待态进程具有较高的优先级并且操作系统已经得知导致它阻塞的事件即将结束,此时便发生了这一状态变化。
就绪态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
等待态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
终止态→NULL:完成善后操作。
进程状态
进程状态 | 描述 |
创建状态 | 进程正在被Linux内核创建 |
就绪 | 进程还没有开始执行,但相关数据已被创建,只要内核调度它就立即可以执行 |
内核状态 | 进程在内核状态下被运行,被调度上CPU执行 |
用户状态 | 进程在用户状态下被运行,等待被调度上CPU执行 |
睡眠 | 进程正在睡眠,等待系统资源或相关信号唤醒 |
唤醒 | 正在睡眠的进程收到Linux内核唤醒的信号 |
被抢先 | 具有更高优先级的进程强制获得进程的CPU时钟周期 |
僵死状态 | 进程通过系统调用结束,进程不再存在,但在进程表项中仍有记录,该记录可由父进程收集 |
进程状态转换
子进程被Linux内核调入CPU执行的过程
最初,父进程通过fork系统调用创建子进程,子进程被创建后,处于创建态。Linux内核为子进程配置数据结构。如果内存空间足够,子进程在内核中就绪,否则在Swap分区就绪。这时子进程处于就绪态,等待Linux内核调度。
Linux内核会为子进程分配CPU时钟周期,在合适的时间将子进程调度上CPU运行,这时子进程处于内核态,子进程开始运行。被分配的CPU时钟周期结束时,Linux内核再次调度子进程,将子进程调出CPU,子进程进入用户态。
待子进程被分配到下一个CPU时钟周期到来时,Linux内核又将子进程调度到CPU运行,使子进程进入内核态。如果有其他进程获得更高的优先级,子进程的时钟周期可能会被抢占,这时又回到用户态。
子进程进入睡眠状态
子进程在运行时,如果请求的资源得不到满足将进入睡眠态,睡眠态的子进程被从内存调换到Swap分区。被请求的资源可能是一个文件,也可能是打印机等硬件设备。如果该资源被释放,子进程将被调入内存,继续以系统态执行。
子进程结束
子进程可以通过exit系统调用结束,这时子进程将进入僵死态,声明周期结束。
子进程在内核中的数据结构又被称为上下文。
上下文:
用户级上下文-子进程用户空间的数据结构;
系统级上下文-子进程内核空间的数据结构;
寄存器上下文-子进程运行时装入CPU寄存器的数据结构。
子进程切换时,CPU收到一个软中断,这时上下文将被保存起来,称之为保存现场。子进程再次运行时,上下文将还原到相关设置,称之为还原现场。整个过程称为上下文切换,保存上下文的数据空间称为u区,是Linux内核为进程分配的存储空间。
内核在以下情况会进行上下文切换操作:
子进程进入睡眠态时;
子进程时钟周期结束,被转为用户态时;
子进程再次被调度上CPU运行,转为系统态时;
子进程僵死时。
进程控制
调度时机:指进程何时被调度上CPU执行。例如,转变为睡眠态的进程将获得较高的优先级,一旦所需要的资源被释放,该进程可以立即被调度上CPU执行。被抢占的进程也将获得一个较高的优先级,抢占其CPU时钟周期的进程一旦转为用户态,被抢占的进程立即转为内核态。
调度算法:所关心的内容就是如何为进程分配优先级。
pid_t fork(void):创建子进程
在执行fork函数之前,操作系统只有一个进程,fork函数之前的代码只会被执行一次。
在执行fork函数之后,操作系统有两个几乎一样的进程,fork函数之后的代码会被执行两次。
执行完fork函数后,fork函数会返回两次。在父进程中,fork返回新创建新进程的进程ID;在父进程中,fork返回0;如果出现错误,返回负值。
//所需头文件
#include <sys/types.h>
#include <unistd.h>
//函数原型
pid_t fork(void); //创建子进程时,复制父进程的上下文
pid_t vfork(void); //创建子进程时,不复制父进程的上下文
//返回值
成功:0或其它正整数
失败:-1
首先,Linux内核在进程表中为子进程分配一个表项,然后分配PID。子进程表项的内容来自于父进程,fork系统调用会将父进程的进程表项复制为副本,并分配给子进程。然后,Linux内核使父进程的文件表和索引表的节点自增1,创建用户级上下文。最后,将父进程上下文复制到子进程的上下文空间中。fork系统调用结束后,子进程的PID被返回给父进程,而子进程获得的值为0。
exec()函数族:运行一个可执行文件。
//常见后缀
l:以列表形式传参
v:以矢量数组形式传参
p:使用环境变量Path来寻找指定执行文件
e:用户提供自定义的环境变量
//所需头文件
#include <unistd.h>
//函数原型
int execl(const char *path,const char *arg,...);
int execlp(const char *file,const char *arg,...);
int execv(const char *path,char *const argv[]);
int execve(const char *path,char *const argv[],char *const envp[]);
//返回值
成功:不返回
失败:-1
exec系统调用会结束原有进程,然后更新上下文的内容,并从头开始执行一个新的进程。两个进程之间并无父子关系。
exec系列函数是直接将当前进程给替换掉的,当调用exec系列函数后,当前进程将不会再继续执行。exec系列函数执行成功不返回,执行错误时,返回-1并且会设置错误变量errno。
fork.c
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char** argv)
{
pid_t i;
printf("before fork:%d\r\n",i);
i=fork();
printf("after fork:%d\r\n",i);
return 0;
}
execl.c
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char** argv)
{
pid_t i;
i=fork();
if(i>0) //父进程执行
{
execl("/bin/ls","ls","-l",NULL);
printf("error!!\r\n");
return -1;
}
return 0;
}
execlp.c
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char** argv)
{
pid_t i;
i=fork();
if(i>0) //父进程执行
{
execlp("ls","-l",NULL);
printf("error!!\r\n");
return -1;
}
return 0;
}
execv.c
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char** argv)
{
pid_t i;
char *arg[]={"ls","-l",NULL};
i=fork();
if(i>0) //父进程执行
{
execv("/bin/ls",arg);
printf("error!!\r\n");
return -1;
}
return 0;
}
execve.c
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char** argv)
{
pid_t i;
char *arg[]={"env",NULL};
char *env[]={"PATH=/tmp","name=couvrir",NULL};
i=fork();
if(i>0) //父进程执行
{
execve("/usr/bin/env",arg,env);
printf("error!!\r\n");
return -1;
}
return 0;
}
void exit(int status):结束进程
//所需头文件
#include <unistd.h>
#include <stdlib.h>
//函数原型
void exit(int status);
void _exit(int status);
//返回值
不返回
进程执行exit系统调用后,Linux内核将删除进程的上下文,但保留进程表项,进程处于僵死态。待合适时,再删除进程表项的内容,释放进程PID。
wait():同步父进程与子进程
//所需头文件
#include <sys/wait.h>
//函数原型
pid_t wait(int *status);
//返回值
成功:退出的子进程PID
失败:-1
//处理子进程退出状态值的宏
WIFEXITED(status):如果子进程正常退出,则该宏为真
WEXITSTATUSA(status):如果子进程正常退出,则该宏获取子进程的退出值
系统调用wait()函数后,父进程的执行被阻断,直到子进程进入僵死态。这时,子进程的退出参数可通过wait()函数返回给父进程。wait系统调用常被用来判断子进程是否已结束。
sleep(秒数):进程睡眠
exit.c
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc,char** argv)
{
pid_t i;
i=fork();
if(i==-1)
printf("fork error!!\r\n");
if(i>0) //父进程执行
{ //_exit()函数直接退出进程
printf("parent!!");
_exit(0);
}else //子进程执行
{ //exit()函数会检查该进程的IO缓存区是否有数据,如果有,则处理IO缓存区的数据才退出进程
printf("son!!");
exit(0);
}
return 0;
}
wait.c
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc,char** argv)
{
pid_t i;
int status;
i=fork();
if(i==-1)
printf("fork error!!\r\n");
if(i>0) //父进程执行
{
wait(&status);
if(WIFEXITED(status)==1)
printf("exit value:%d\r\n",WEXITSTATUS(status));
return 0;
}else //子进程执行
{
printf("son!!");
exit(0);
}
return 0;
}
进程组PGID:对相同类型的进程进行管理,由首进程PID决定
在Shell里面直接执行一个应用程序,对于大部分进程来说,自己就是进程组的首进程,进程组只有一个进程。如果进程调用了fork函数,那么父子进程同属一个进程组,父进程为首进程。
在Shell中通过管道执行连接起来的应用程序,两个程序同属一个进程组,第一个程序为进程组的首进程。
会话SID:管理进程组,由会话首进程决定
调用setsid函数,新建一个会话,应用程序作为会话的第一个进程,称为会话首进程。
用户在终端正确登录后,启动Shell时linux系统会创建一个新的会话,Shell进程作为会话首进程。
前台进程组
Shell进程启动时,默认是前台进程组的首进程。前台进程组的首进程会占用会话所关联的终端来运行,Shell启动其他程序时,其他程序成为首进程。
在Shell进程里启动程序时,把后台进程组切换为前台进程组操作:fg jobid
后台进程组
后台进程中的程序不会占用终端。
jobs:查看有哪些后台进程组。
在Shell进程里启动程序时,指定程序运行在后台进程组操作:&、Ctrl+Z。
终端
当终端被关闭之后,会话中的除守护进程外的所有进程都会被关闭。
物理终端:串口终端、LCD终端
伪终端:SSH远程连接产生的终端、桌面系统启动的终端
虚拟终端:Linux内核自带,Ctrl+Alt+F0~F6可以打开7个虚拟终端
守护进程
创建子进程,父进程直接退出(父进程放弃终端使用权)。fork()函数。
创建新会话,不关联任何终端。setsid()函数。
改变守护进程的当前工作目录,改为“/”。chdir()函数。
重设文件权限掩码,新建文件的权限受文件权限掩码影响。umask()函数
关闭默认的文件描述符。close()函数。
实现守护进程的功能。
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAXFILE 3
int main(int argc,char** argv)
{
pid_t pid;
int fd,len,i,num;
char *buf="the daemon is running\n";
len=strlen(buf)+1;
/*1.创建子进程,销毁父进程*/
pid=fork();
if(pid<0)
{
printf("fork error!!\r\n");
exit(1);
}
if(pid>0) //父进程执行
exit(0);
/*2.创建新会话,不关联任何终端*/
setsid();
/*3.改变当前工作目录*/
chdir("/");
/*4.重设文件权限掩码*/
umask(0);
/*5.关闭默认的文件描述符*/
for( i=0 ; i<MAXFILE ; i++ )
close(i);
/*6.实现守护进程的功能*/
while(1)
{
fd = open("/var/log/daemon.log", O_CREAT | O_WRONLY | O_APPEND, 0666);
write(fd, buf, len);
close(fd);
sleep(5);
}
}
按照视频教学来的,没实现功能,不知道为什么? 把代码贴上。
普通程序伪装成守护进程:nohup
僵死进程
进程正常退出:
子进程调用exit()函数退出
父进程调用wait()函数为子进程处理其他事情
子进程调用exit()函数退出,父进没有调用wait()函数为子进程处理其他事情,子进程就变成僵死进程。
托孤进程
父进程比子进程先退出(_exit()可立即退出进程),子进程变为孤儿进程,Linux系统会把子进程托孤给1号进程(init进程)。