赵老师笔记----进程
一、什么是进程
进程就是活动的程序,是程序的运行实例。
几个层次
任务-进程-线程
LINUX是一个多任务的系统(task)
通过ps -el可以查看到系统中的任务
通过ps -el|wc -l 可以统计任务数
通过 ps -aux 查看,其中用"[]"括起来为内核线程
在LINUX中,用户空间的进程、线程、内核空间中的内核线程都对应着内核中的一个任务
操作系统基本都支持多任务,将CPU划分时间片,通过时间片轮转在不同的时间片去执行不同的任务,宏观看起来它就同时在执行多个任务。
每个任务又有多个进程构成去共同完成。
一个进程又由至少一个线程构成。
进程是管理事务的基本单元。进程拥有自己独立的处理环境和系统资源(处理器、存储器、I/O设备、数据、程序)。
1.定义:
进程的特点:
进程是一个独立的可调度的任务
进程是程序的一次执行过程
进程是资源管理的最小单位(比如,打开的文件)
进程有自己独立的4GB虚拟内存空间
进程和程序异同:
差别:
程序是静态,是存储在存储介质中的指令和数据的集合
进程是动态的,进程是程序的一次执行过程,它包括进程的创建、运行和消亡过程
相同的点:
程序和进程都是按照ELF格式分段进行管理的
多进程编程:
进程号:在系统中唯一的一个数字,代表一个进程, pid
父进程号:Parent ProcessID, ppid
进程中可以通过getpid()和getppid()获取进程ID号和父进程号
一个系统中的进程的关系是一个树状结构(通过pstree命令可以看到)
2.进程的状态
进程整个生命周期可以简单划分为三种基本状态:
就绪态:
进程已经具备执行的一切条件,正在等待分配CPU的处理时间。
执行态:
该进程正在占用CPU运行。
等待态:
进程因不具备某些执行条件而暂时无法继续执行的状态。
进程三种状态的转换关系
就绪态--cpu时间片轮转到,进程调度到cpu执行-->执行态
执行态--cpu时间片用完,被调度出cpu-->就绪态
执行态--cpu时间片未用完,但接下去动作需要的资源被别人占用-->等待态
等待态--所需资源被释放,当所有资源准备好时-->就绪态
进程状态切换:
运行态(running): 正在CPU上运行的进程状态
就绪态(ready): 其他条件已经具备,只差被调度器调度到CPU上运行
挂起状态(suspended):进程执行过程总中由于等待某些外部IO条件(比如读数据时,外设暂没数据可读)
停止态(stopped):进程通过“停止”信号进入的状态(比如遇到GDB里面的断点)
僵尸态(Zombie): 应用程序已经退出(应用空间的内存已经释放),但在内核中所占8KB的物理内存没有释放(在特定情况下才出现,写程序时注意避免)
3.进程控制块(PCB)
①PCB是进程存在的唯一标志,在Linux中PCB存放在task_struct结构体中。
②PCB是操作系统中最重要的记录型数据结构。PCB中记录了用于描述进程进展情况及控制进程运行所需的全部信息。
③OS是根据PCB来对并发执行的进程进行控制和管理的。系统在创建一个进程的时候会开辟一段内存空间存放与此进程相关的PCB数据结构。
task_struct 包含了这些内容:
(1)标示符 : 描述本进程的唯一标识符,用来区别其他进程。
(2)状态 :任务状态,退出代码,退出信号等。
(3)优先级 :相对于其他进程的优先级。
(4)程序计数器:程序中即将被执行的下一条指令的地址。
(5)内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
(6)上下文数据:进程执行时处理器的寄存器中的数据。
(7)I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
(8)记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
struct task_struct {
volatile long state; /* 任务状态: -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
int prio, static_prio, normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
#ifdef CONFIG_PREEMPT_NOTIFIERS
/* list of struct preempt_notifier: */
struct hlist_head preempt_notifiers;
#endif
unsigned int policy;
struct list_head tasks;
struct mm_struct *mm, *active_mm; /* 指向用户空间的虚拟内存: 每个进程指向不同的地址(形成每个进程不同的用户虚拟内存【3GB】),同一进程的不同线程指向同一地址, 内核线程mm为NULL*/
/* task state */
int exit_state; /* 退出状态*/
int exit_code, exit_signal; /* 退出值,退出信号*/
int pdeath_signal; /* The signal sent when the parent dies */
unsigned int group_stop; /* GROUP_STOP_*, siglock protected */
/* ??? */
unsigned did_exec:1;
/* Revert to default priority/policy when forking */
unsigned sched_reset_on_fork:1;
unsigned sched_contributes_to_load:1;
pid_t pid;
pid_t tgid;
/*
* pointers to (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/
struct task_struct *real_parent; /* real parent process */
struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */
/*
* children/sibling forms the list of my natural children
*/
struct list_head children; /* list of my children */
struct list_head sibling; /* linkage in my parent's children list */
struct task_struct *group_leader; /* threadgroup leader */
/* PID/PID hash table linkage. */
struct pid_link pids[PIDTYPE_MAX];
struct list_head thread_group;
struct completion *vfork_done; /* for vfork() */
//...
④PCB的功能
调度数据
进程的状态、标志、优先级、调度策略等。
时间数据
创建该进程的时间、在用户态的运行时间、在内核态的运行时间等。
文件系统数据
umask掩码、文件描述符表等。
内存数据、进程上下文、进程标识(进程号)
...
二、进程的创建
0.进程号
①如何用命令查看进程号
ps -e
②如何杀死进程
kill 进程号
③进程号
每个进程都由一个进程号来标识,其类型为pid_t,进程号的范围:0~32767。
进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用了。
在linux系统中进程号由0开始。
进程号为0及1的进程由内核创建。
进程号为0的进程通常是调度进程,常被称为交换进程(swapper)。进程号为1的进程通常是init进程。
除调度进程外,在linux下面所有的进程都由进程init进程直接或者间接创建。
pstree 命令去查看进程依赖关系
④进程号分类
进程号(PID):标识进程的一个非负整型数。范围:0~32767
父进程号(PPID): 任何进程(除init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。
进程组号(PGID):
进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID) 。
⑤在自己编写的程序里如何获得进程号
getpid()、getppid()、getpgid()。
例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
pid_t pid, ppid, pgid;
pid = getpid();
printf("pid = %d\n", pid);
ppid = getppid();
printf("ppid = %d\n", ppid);
pgid = getpgid(pid);
printf("pgid = %d\n", pgid);
return 0;
}
1.直接创建进程
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
pid_t vfork(void);
①fork创建子进程
pid_t fork(void);
返回值==0,在子进程里
返回值>0,返回的是子进程的pid,表示在父进程里
返回值<0,创建子进程失败
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid=fork();
if (pid<0)
perror("fork");
if (0==pid)
{//子进程里
while(1)
{
printf("in son process!\n");
sleep(1);
}
}
else
{//父进程里
while(1)
{
printf("in father process!\n");
sleep(1);
}
}
//公共部分
printf("common code area\n");
return 0;
}
使用fork时:
使用fork函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间。
地址空间:包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。
子进程所独有的只有它的进程号,计时器等。因此,使用fork函数的代价是很大的。
在重定向父进程的标准输出时,子进程的标准输出也被重定向。
进程在一定的时间内没有任何动作,称为进程的挂起
②sleep函数的使用
#include <unistd.h>
unsigned int sleep(unsigned int sec);
功能:
进程挂起指定的秒数,直到指定的时间用完或收到信号才解除挂起。
返回值:
若进程挂起到sec指定的时间则返回0,若有信号中断则返回剩余秒数。
注意:
进程挂起指定的秒数后程序并不会立即执行,系统只是将此进程切换到就绪态。
③vfork创建子进程
vfork函数:创建一个新进程
pid_t vfork(void)
功能:
vfork函数和fork函数一样都是在已有的进程中创建一个新的进程,但它们创建的子进程是有区别的。
返回值:
创建子进程成功,则在子进程中返回0,父进程中返回子进程ID。出错则返回-1。
fork和vfork函数的区别:
vfork保证子进程先运行,在它调用exec或exit之后,父进程才可能被调度运行。
vfork和fork一样都创建一个子进程,但它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不访问该地址空间。相反,在子进程中调用exec或exit之前,它在父进程的地址空间中运行,在exec之后子进程会有自己的进程空间。
vfork()的特点:
1. vfork()后父进程会挂起直到子进程退出后才能继续运行
2. 父子进程共享数据段(子进程的数据段直接指向了父进程)
3. 子进程退出时要使用_exit()或exit()函数(避免段错误)
vfork()比较少用
④父子进程有时需要简单的进程间同步,如父进程等待子进程的结束。利用wait回收子进程资源。
linux下提供了以下两个等待函数wait()、waitpid()
pid_t wait(int *status);
功能:
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
调用wait函数的进程会挂起,直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒。
若调用进程没有子进程或它的子进程已经结束,该函数立即返回。
pid_t waitpid(pid_t pid, int *status,
int options);
功能:
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
返回值:
如果执行成功则返回子进程ID。
出错返回-1,失败原因存于errno中。
取出子进程的退出信息
WIFEXITED(status)
如果子进程是正常终止的,取出的字段值非零。
WEXITSTATUS(status)
返回子进程的退出状态,退出状态保存在status变量的8~16位。在用此宏前应先用宏WIFEXITED判断子进程是否正常退出,正常退出才可以使用此宏。
注意:此status是个wait的参数指向的整型变量。
僵尸进程(Zombie Process):进程已运行结束,但进程的占用的资源未被回收,这样的进程称为僵尸进程。子进程已运行结束,父进程未调用wait或waitpid函数回收子进程的资源是子进程变为僵尸进程的原因。
孤儿进程(Orphan Process): 父进程运行结束,但子进程未运行结束的子进程。
守护进程(精灵进程)(Daemon process):守护进程是个特殊的孤儿进程,这种进程脱离终端,在后台运行
⑤结束进程相关函数
在linux下可以通过以下方式结束正在运行的进程:
void exit(int value);
void _exit(int value);
区别:
exit为库函数,而_exit为系统调用
exit会执行退出注册函数,并刷新缓存区。
如何组成退出函数?
atexit(函数名);
一个进程中可以多次调用atexit函数注册清理函数,正常结束前调用函数的顺序和注册时的顺序相反。
2.进程替换(exec系列函数)
exec函数族,是由六个exec函数组成的。
exec函数族提供了六种在进程中启动另一个程序的方法。
exec函数族可以根据指定的文件名或目录名找到可执行文件。
调用exec函数的进程并不创建新的进程,故调用exec前后,进程的进程号并不会改变,其执行的程序完全由新的程序替换,而新程序则从其main函数开始执行。
①六个函数
#include <unistd.h>
int execl(const char *pathname,const char *arg0,…,NULL);
int execlp(const char *filename, const char *arg0,…,NULL);
int execle(const char *pathname,const char *arg0,…,NULL,char *const envp[]);
int execv(const char *pathname,char *const argv[]);
int execvp(const char *filename,char *const argv[]);
int execve(const char *pathname,char *const argv[],char *const envp[]);
execl、execv
l代表list ,v 代表向量vector
p代表PATH
e代表环境变量
②int system(const char *command);