概述
- 程序是存放在存储介质上的一个可执行文件
- 进程是程序执行的过程
- 进程的一次执行过程,其状态是变化的,其包括进程的创建、调度和消亡
- 在 Linux 系统中,进程是管理事务的基本单元。进程拥有自己独立的处理环境和系统资源。
- 可使用exec族函数可以由内核将程序读入内存,使其执行起来成为一个进程
进程状态
- Linux下进程有5种运行状态:R 运行状态 ,S可中断等待状态 ,D不可中断等待状态,Z僵尸状态,T停止状态
- 按进程运行的整个生命周期可以将进程简单划分为三种状态:
- 就绪态:进程已经具备执行的一切条件,正在等待分配 CPU 的处理时间。
- 运行态:该进程正在占用 CPU 运行
- 阻塞态:正在执行的进程由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞态
- 进程调度机制:时间片轮转,上下文切换
进程控制块
- 当进程执行时,系统会开辟一段内存空间存放与此进程相关的数据信息,而这个数据信息是通过结构体(task_struct)来存放,我们把这个存放进程相关数据信息的结构体称为进程控制块。操作系统就是通过这个进程控制块来操作控制进程。
- 进程控制块存放在内核中(/usr/src/linux-headers-4.15.0-30/include/linux/sched.h)
ps命令(shell)
-
Linux中的ps命令是Process Status的缩写。ps命令用来列出系统中当前运行的那些进程 -
ps 为我们提供了进程的一次性的查看,它所提供的查看结果并不动态连续的;如果想对进程时间监控,应该用 top 工具 -
ps命令:ps [参数] 用来显示当前进程的状态 命令参数:
参数 | 含义 |
---|
a | 显示所有进程 | -a | 显示同一终端下的所有程序 | -A | 显示所有进程 | c | 显示进程的真实名称 | N | 反向选择 | e | 等于“-A” | e | 显示环境变量 | f | 显示程序间的关系 | H | 显示树状结构 | r | 显示当前终端的进程 | T | 显示当前终端的所有程序 | u | 指定用户的所有进程 | -au | 显示较详细的资讯 | -aux | 显示所有包含其他使用者的行程 | -C<命令> | 列出指定命令的状况 | –lines<行数> | 每页显示的行数 | –width<字符数> | 每页显示的字符数 | –help | 显示帮助信息 | –version | 显示版本显示 |
进程号
-
每个进程都由一个进程号来标识,其类型为 pid_t(无符号整型),由系统随机分配,进程号的范围:0~32767 -
进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用 -
进程号PID:标识进程的一个非负整型数 -
父进程号PPID:任何进程( 除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号 -
进程组号PGID: 进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号 -
PID 0 为交换进程swapper,进行进程调度 -
PID 1 为init进程,进行系统初始化,收留孤儿进程 -
获取进程号的函数: #include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
pid_t getpgid(pid_tpid);
进程的虚拟地址空间
- 每个进程都有自己独立的4G内存空间,但实际上这个内存是不存在的,是虚拟内存,每次访问内存空间地址的时候,都需要翻译成实际物理地址
- 栈stack:存放局部变量,函数参数
- 堆heap:动态分配内存
- 未初始化数据段bss:未初始化的变量
- 数据段static:全局变量,静态变量,静态局部变量
- 代码段code: 包含了进程运行的程序机器语言指令。文本段具有只读属性,以防止进程通过错误指针意外修改自身指令
- 系统之所以分成这么多个区域,主要基于以下考虑:
- 代码段和数据段分开,运行时便于分开加载,在哈佛体系结构的处理器将取得更好得流水线效率。
- 代码时依次执行的,是由处理器 PC 指针依次读入,而且代码可以被多个程序共享,数据在整个运行过程中有可能多次被调用,如果将代码和数据混合在一起将造成空间的浪费。
- 临时数据以及需要再次使用的代码在运行时放入栈中,生命周期短,便于提高资源利用率。
- 堆区可以由程序员分配和释放,以便用户自由分配,提高程序的灵活性。
创建一个进程fork()
-
一个进程,包括代码、数据和分配给进程的资源。fork函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。 -
一个进程调用fork函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。 -
当一个c程序使用fork后,fork上面的代码里的数据两进程不共享,相互独立;下面的代码两个进程都会执行,如果要两进程执行不同任务需用fork的返回值区分 -
函数原型: #include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
-
功能:用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程 -
返回值:
- 成功:返回两个值,子进程中返回 0,父进程中返回子进程号
- 失败:返回-1
-
如果父进程先于子进程完成退出,此时子进程失去父亲成为孤儿进程由init进程(PID 1)成为其新的父进程 -
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("before fork, pid:[%d]\n", getpid());
pid_t pid = fork();
if(pid<0)
{
perror("fork error");
return -1;
}
else if(pid>0)
{
printf("father: [%d], pid==[%d], fpid==[%d]\n", pid, getpid(),getppid());
}
else if(pid==0)
{
printf("child: pid==[%d], fpid==[%d]\n", getpid(), getppid());
}
printf("after fork, pid:[%d]\n", getpid());
return 0;
}
进程挂起
进程等待
-
进程已运行结束,但进程的占用的资源未被回收,这样的进程称为僵尸进程 -
当一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号,而父进程可以通过 wait() 或 waitpid()函数等待子进程结束,获取子进程结束时的状态,同时回收他们的资源 -
父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。 -
原型: #include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
wait:
waitpid():
wait示例:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>
#include <errno.h>
#include <stdlib.h>
void waitprocess();
int main(int argc, char * argv[])
{
waitprocess();
}
void waitprocess()
{
int count = 0;
pid_t pid = fork();
int status = -1;
if(pid<0)
{
printf("fork error for %m\n",errno );
}else if(pid>0) {
printf("this is parent ,pid = %d\n",getpid() );
wait(&status);
}else{
printf("this is child , pid = %d , ppid = %d\n",getpid(),getppid());
int i;
for (i = 0; i < 10; i++){
count++;
sleep(1);
printf("count = %d\n", count);
}
exit(5);
}
printf("child exit status is %d\n", WEXITSTATUS(status));
printf("end of program from pid = %d\n",getpid() );
}
进程退出
函数:
#include <stdlib.h>
void exit(int status);
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);
- return仅调用堆栈的返回,将控制权交给主调函数,只在主函数里相当于exit;使用exit结束进程将控制权交给操作系统或父进程
- exit()属于标准库函数,_exit()属于系统调用函数,调用 exit() 函数,会刷新 I/O 缓冲区而_exit()不会(使用_exit函数退出printf的调试信息可能不会被打印)
使用vfork创建进程
函数:
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
- 子进程共享父进程的地址空间,所以vfork不会克隆父进程,子进程运行时父进程被挂起
- vfork保证子进程先运行,在它调用 exec(进程替换) 或 exit(退出进程)之后父进程才可能被调度运行。如果子进程没有调用 exec, exit, 程序则会导致死锁
- 由vfork创建进程时与父进程公用一块地址空间,那么他的创建进程的效率很高。但是fork后来实现了写时拷贝技术,他的效率大大提高,而且进程独立,所以vfork就被淘汰来
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
pid_t pid;
pid = vfork();
if(pid < 0){
perror("vfork");
}
if(0 == pid){
sleep(3);
printf("i am son\n");
_exit(0);
}else if(pid > 0){
printf("i am father\n");
}
return 0;
}
进程替换:exec函数族
-
exec函数族可以在进程中启动另一个程序,它可以根据指定的文件名或目录名找到可执行文件 -
被启动的程序取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了,所以通常fork一个新进程给exec执行 -
被启动的程序除了二进制文件,也可以是任何Linux下可执行的脚本文件,所以shell命令也可以用exec执行 -
函数原型: #include <unistd.h>
int execl(const char *path, const char *arg, ...
);
int execlp(const char *file, const char *arg, ...
);
int execle(const char *path, const char *arg, ...
);
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[]);
-
函数返回值:调用成功函数不会返回;出错返回-1,失败原因记录在error中 -
exec族函数一个有6个,其中只有 execve() 是真正意义上的系统调用,其它都是在此基础上经过包装的库函数
exec后面的字母:
参数说明:
- 带l execl,execlp,execle:表示后边的参数以可变参数的形式给出且都以一个空指针结束。这里特别要说明的是,程序名也是参数,所以第一个参数就是程序名
- 带p execlp,execvp:表示第一个参数无需给出具体的路径,只需给出函数名即可,系统会在PATH环境变量中寻找所对应的程序,如果没找到的话返回-1
- 带v execv,execvp:表示命令所需的参数以char *arg[]形式给出且arg最后一个元素必须是NULL
- 带e execle:将环境变量传递给需要替换的进程,原来的环境变量不再起作用
以PS举例:
char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};
char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execv("/bin/ps", ps_argv);
execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);
execve("/bin/ps", ps_argv, ps_envp);
execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execvp("ps", ps_argv);
|