进程总结
1.进程概念
-
进程是运行中的程序,程序是保存在硬盘上的可执行代码
-
进程内部又划分了许多线程,线程基本上不拥有系统资源,他与同属一个进程的其他线程共享进程的全部资源。
-
进程在执行过程中拥有独立的内存单元。内部线程可共享这些内存,一个线程可以创建和撤销另一个线程,同一个线程中的多个进程可以并行执行。
-
***ps或pstree***可以查看当前的进程
2.进程标识
获得进程各种标识符的函数表
#include<unistd.h>
函数声明 | 功能 |
---|---|
pid_t getpid(id) | 获得进程ID |
pid_t getppid(id) | 获得父进程ID |
pid_t getuid() | 获得进程的实际用户ID |
pid_t geteuid() | 获得进程的有效用户ID |
pid_t getgid() | 获得进程的实际组ID |
pid_t getegid(id) | 获得进程的有效组ID |
- 某个普通用户A,运行了一个程序,而这个程序是以root身份来运行的,这程序就有root权限。实际:A的ID,有效:root的ID
3.进程结构
-
名称 内容 代码段 存放程序可执行代码 数据段 存放全局变量,常量,静态变量 堆 存放动态分配的内存变量 栈 用于函数调用,存放函数的参数,函数内部定义的局部变量
4.进程状态
-
运行状态®:正在运行或在运行队列中等待运行
-
可中断等待状态(S): 进程正在的等待某个事件完成(如等数据到达),等待过程中可以被信号或定时器唤醒。
-
不可中断等待状态(D): 等待过程中不可以被唤醒,必须等待直到等待的事情发生
-
濒死状态(Z):进程已终止,进程描述符依然在,直到父进程调用wait函数后释放
-
停止状态(T):进程因为受到信号(SIGSTOP,SIGSTP,SIGTIN,SIGTOU)后停止运行,或者该进程正在被跟踪(调式程序时,进程处于被跟踪的状态)
-
ps -eo pid,user,args
参数 -e 显示所有进程信息,-o 参数控制输出。Pid,User 和 Args参数显示PID,运行应用的用户和该应用
-
<(高优先级进程) N(低优先级进程) L(内存锁页,即页不可以被唤出内存) s(该进程为会话首进程) l(多线程进程) +(进程位于前台进程组)
-
进程控制
- 有:创建进程,执行新程序,退出进程,改变进程优先级
- 对进程进行控制的主要系统调用:
- fork:创建一个新进程
- exit:终止进程
- exec:执行一个应用程序
- wait:将父进程挂起,等待子进程终止
- getpid:获取当前进程的进程ID
- nice:改变进程优先级
创建进程
-
fork
-
作用:创建进程
-
原型:
#include<sys/types.h> #include<unistd.h> pid_t fork(void);
-
两个返回值(调用一次返回两次)
-
调用fork函数后,当前进程实际上已经分裂成两个进程了,一个是原来的父进程,一个是刚创建的子进程。父子进程在fork函数的地方分开。
-
两个返回值:
- 父进程的返回值是刚创建的子进程的ID
- 子进程的返回值是0
-
创建成功:fork函数返回两次
-
创建失败:返回-1
当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。 系统内存不足,这时errno的值被设置为ENOMEM。
-
-
父子进程是交替运行的,谁先运行取决于内核使用的调度算法。(因为操作系统一般让进程享有同等执行权)
-
子进程继承的父进程的有:
- 用户ID,组ID,当前工作目录,根目录,打开的文件,创建文件中使用的屏蔽字,信号屏蔽字,上下文环境,共享的存储段,资源限制等
-
子进程有一些自己的特性:
- 子进程有ta唯一的ID
- fork的返回值不同,父进程返回子进程的ID,子进程返回0
- 不同父进程的ID
- 子进程共享父进程打开的文件描述符,但父进程对文件描述符的改变不会影响子进程中的文件描述符
- 子进程不能继承父进程设置的文件锁
- 子进程不继承父进程设置的警告
- 子进程的未决信号集被清空
- 未决信号集:没有处理信号的集合
- 未决:一种状态,指的是从信号产生到信号被处理前的这段时间
- 阻塞信号集:要屏蔽的信号的集合
- 阻塞:是一个开关的动作,指的是阻止信号被处理,但不是阻止信号产生
- 未决信号集:没有处理信号的集合
-
孤儿进程
-
如果一个子进程的父进程先于子进程结束,子进程就成为了一个孤儿进程,ta由init进程(1)收养,成为init进程的子进程。
#include<stdio.h> #include<stdlib.h> #include<sys/types.h> #include<unistd.h> int main(void) { pid_t pid; pid=fork(); switch(pid) { case 0: { while(1) { printf("A background process,PID:%d\n,parentID:%d\n",getpid(),getppid()); sleep(3); } } case -1: { perror("process creation failed\n"); exit(-1); } default: { printf("I am parent process,my pid is %d\n",getpid()); exit(0); } } return 0; }
僵尸进程
- 一个进程使用fork创建子进程,如果子进程退出,而父进程没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,此进程称为僵尸进程
vfork
- vfork与fork区别:
- 都是调用一次返回两次
- fork:创建一个子进程,子进程完全复制父进程的资源,得到的子进程独立于父进程
- fork:创建一个子进程,操作系统并不是把父进程的地址空间完全复制给子进程。创建的子进程共享父进程的地址空间,即子进程完全运行在父进程的地址空间,子进程对地址空间中任何数据的修改同样为父进程所见。
- fork:那个进程先运行取决于系统
- vfork:保证子进程先运行,当它调用exit或exec后,父进程才可以运行。如果在使用exec或exit之前子进程要依赖于父进程的某个行为,就会导致死锁
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
int globVar=5;
int main(void)
{
pid_t pid;
int var=1;
int i;
printf("fork is differernt with vfork\n");
pid=fork();
//pid=vfork();
switch(pid)
{
case 0:
{
i=3;
while(i-->0)
{
printf("child process is running\n");
globVar++;
var++;
sleep(1);
}
printf("child's globVar=%d,var=%d\n",globVar,var);
exit(0);
}
case -1:
{
perror("process creat default\n");
exit(0);
}
default:
{
i=5;
while(i-->0)
{
printf("parent process is running\n");
globVar++;
var++;
sleep(1);
}
printf("parent's globVar=%d,var=%d\n",globVar,var);
exit(0);
}
}
}
对fork:不管是全局变量还是局部变量,子进程与父进程对他们的修改互不影响。
对vfork:子进程修改变量对父进程是可见的(子进程先执行)
进程退出
-
不管是那种退出方式,最终都会***执行内核中的同一段代码***。这段代码用来关闭进程所有已打开的文件描述符,释放它所占用的内存和其他资源。
-
在main函数中执行return 调用about函数 调用exit函数 进程收到某个信号,而该信号使程序终止 调用_exit函数 在main函数中执行return -
比较:
方式 特点 exit 1.是一个函数,有参数 2.控制权交给系统 3.exit(0):正常退出 exit(1):执行过程中有错误发生 eg:溢出,除数为0 4.头文件#incldue<stdlib.h> 5.exit()退出进程会清理I/O缓冲区 return 1.函数执行完后的返回 2.控制权交给调用函数 _exit 1.是一个函数 2.头文件#include<unistd.h> 3._exit()退出直接结束到内核中
- eg:
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
printf("ok!\n");
printf("yes");
exit(0);
}
#include<stdio.h>
#include<unistd.h>
int main(void)
{
printf("ok!\n");
printf("yes");
_exit(0);
}
原因:printf函数使用的是***缓冲I/O***的方式,在遇到‘\n’换行符时自动从缓冲区中将记录读出来。
exit是将缓冲区的数据读出来后才能退出,所以调用exit函数后程序并不会马上退出,所以就会导致出现僵尸程序。
_exit是直接退出进入到内核中去
执行新程序
-
使用fork或vfork创建子进程后,子进程通常会调用exec函数来执行另一个程序,系统调用exec用于执行一个可执行程序以代替当前进程的执行映像
-
exec调用并没有生成新程序,一个进程一旦调用exec函数,ta本身就’死亡了‘,系统把代码段替换成新的程序的的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段和堆栈段,唯一保留的就是进程ID。也就是说,还是同一个进程,不过执行的是另一个程序。
-
linux下,exec函数族有6种不同的调用形式,头文件#include<unistd.h>
#include<unistd.h>
int execve(const char *path,char * const argv[],char *const envp[]);
int execv(const char *path,char * const envp[]);
int execle(const char * path,const char *arg,...);
int execl(const char *path,const char *arg,...);
int exevp(const char *file,char *const argv[]);
int execlp(const char *file,const char *arg,...);
-
环境变量:系统预定义的参数。定义了用户的工作环境
eg:在shell中执行命令,你只要打入命令名,不用打路径。是因为系统在环境变量中,将变量搜索的路径全列在环境变量中。
-
env命令可查看环境变量值,用户也可以修改这些变量值以定制自己的工作环境。
#include<stdio.h>
#include<malloc.h>
extern char **environ;
int main(int argc,char *argv[])
{
int i;
printf("Argument:\n");
for(i=0;i<argc;i++)
printf("argc[%d] is %s\n",i,argv[1]);
printf("Environment:\n");
for(i=0;environ[i]!=NULL;i++)
printf("%s\n",environ[i]);
return 0;
}
结果如下:
而是用env命令执行的结果如下:
所以结果都是一样的
-
environ 是系统与定义的全局变量,用来显示各个环境变量的值
-
类型是char **,存储着系统的环境变量
-
下面介绍个exec函数是如何将main函数需要的参数传递给ta的:
-
argv和envp两个参数的大小都是受限制的,Linux操作系统通过宏ARG_MAX来限制他们的大小,如果他们的容量之和超过ARG_MAX定义的值,将会发生错误。这个宏定义在<linux/limits.h>头文件中
exev函数:execv函数通过路径名方式调用可执行文件作为新的进程映像。他的argv参数用来提供给main函数的argv参数。argv参数是一个以NULL结尾(最后一个元素必须是空指针)的字符串数组 execve函数:参数path是将要执行的程序的路径名,参数argv,envp与main函数的argv,envp对应 execl函数:与execv函数用法相似,只是在传递argv参数的时候,每个命令行参数都声明一个单独的参数,注意这些参数要以一个空指针作为结束 execle函数:与execl函数用法相似,只是要显示指定环境变量。环境变量位于命令行参数最后一个参数的后面,即空指针后面,也就是位于空指针之后。 5.execvp函数:与execv用法一致,不同的是参数filename。该函数如果包含'/'就相当于路径名;如贵哦不包含,函数就到PATH环境变量定统一的目录中去找可执行文件。 execlp函数:类似于execl函数,他们的区别和execvp与execv的区别是一样的。
只有execve是系统调用,其他都是库函数,他们实现时都调用excve函数。
- 正常情况下这些函数是不会返回的,因为进程的执行映像已经被替换,没有接受返回值的地方了。如果错误(文件名和参数错误),返回-1。
- exec函数错误表
errno 错误描述 EACCES 指向的文件或脚本文件没有设置可执行位,即指定的文件是不可执行的 E2BIG 新程序的命令行参数与环境容量之和超过ARG_MAX ENOEXEC 由于没有正确的格式,指定的文件无法正常执行 ENOMEM 没有足够的内存空间执行指定的程序 ETXTBUSY 指定的文件被一个或多个进程以可写的方式打开 EIO 从文件系统读入文件时发生I/O操作 - exec函数族可以执行二进制可执行文件,也可以执行shell脚本程序,但是shell脚本必须以下面的格式开头:#!interpretername [arg]
- interprewtername:shell或其他解释器。eg:/bin/sh /usr/bin/perl
- arg:是传递给解释器的参数
-
演示exec函数的用法:
execve.c
#include<stdlib.h>
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(int argc,char *argv[],char **environ)
{
pid_t pid;
int stat_val;
printf("exec example!\n");
pid=fork();
switch(pid)
{
case -1:
{
perror("process creation failed\n");
exit(1);
}
case 0:
{
printf("child process is running\n");
printf("My pid=%d,parentpid=%d\n",getpid(),getppid());
printf("uid=%d,gid=%d\n",getuid(),getgid());
execve("processimage",argv,environ);
printf("process never go to here!\n");
exit(0);
}
default:
{
printf("parent process is running\n");
break;
}
}
wait(&stat_val);
exit(0);
}
processimage.c
/*用来替换进程映像的程序*/
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main(int argc,char *argv[],char **environ)
{
int i;
printf("i am a process image!\n");
printf("my pid=%d,parentpid=%d\n",getpid(),getppid());
printf("uid=%d,gid=%d\n",getuid(),getpid());
for(i=0;i<argc;i++)
{
printf("argv[%d]:%s\n",i,argv[i]);
}
}
编译程序:gcc -o processimage processimage.c gcc -o execve execve.c
-
从执行结果可以看出:执行新程序的进程保持了原来进程的进程ID,父进程ID,实际用户ID,实际组ID,同时我们还可以看到当调用新的可执行程序后,原有的子进程的映像被替代,不再被执行。所以子进程无法执行到printf(“process never go to here!\n”);因为子进程已经被新的执行映像所替代。
-
执行新程序的进程保持了原有程序的:
原来的进程ID,父进程ID,实际用户ID,实际组ID 当前工作目录 根目录 创建文件时使用的屏蔽字 进程信号屏蔽字 未决警告 和进程相关的使用处理器的时间 控制终端 文件锁 等待进程结束(wait和waitpid)
- 函数原型:
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int *statloc); pid_t waitpid(pid_t pid,int *statloc,int options);
- wait和waitpid所返回的终止状态的宏
头文件sys/wait.h中定义了解读进程退出状态的宏
宏定义 说明 WIFEXITED(stat_val) 如果子进程是正常结束的,该宏返回一个非0值,表示真。若异常结束,返回0,表示假 WEXITSTTUS(stat_val) 若WIFEXITED返回值非0,它返回子进程中exit或_exit参数的低八位 WIFSIGNALED(stat_val) 若子进程异常终止,它就取得一个非0值,表示真 WTERMSIG(stat_val) 如果宏WIFSIGNALED的值非0,该宏返回使子进程异常终止的信号编号 WIFSTOPPED(stat_val) 若子进程暂停,它就取得一个非0值,表示真 WSTOPSIG(stst_val) 若WIFSTOPPED非0,它返回使子进程暂停的信号编号 -
wait:
- 作用:
- 父进程暂停执行,直到它的一个子进程结束为止
- 返回值:
- 终止运行的子进程的PID
- 参数statloc:指向的变量存放子进程的退出码,即从子进程的main函数返回的值或者子进程中exit函数的参数。如果statloc不是一个空指针,状态信息将被写入他指向的变量。
- 作用:
-
waitpid:
-
作用:也用来等待子进程的结束,用来等待某个特定进程结束。
-
参数:
- pid:指明要等待的子进程的PID,pid的意义接下表。
取值 意义 pid>0 等待其进程ID等于pid的子进程退出 pid=0 等待其组ID等于调用进程的组ID的任一子进程 pid<-1 等待其组ID等于pid绝对值的任一子进程 pid=-1 等待任一组进程 - statloc:与wait函数中的参数一样
- options:允许用户改变waitpid的行为,若将该参数赋值为WNOHANG,则使父进程不被挂起而立即返回并执行其后的代码。
-
如果想让父进程周期性的检查某个特定的子进程是否已经退出,可以:
waitpid(child_pid,(int *)0,WNOHANG);
如果子进程尚未退出,它将返回0
如果子进程已经结束,返回child_pid
调用失败返回-1(没有孩子进程,参数不合法等)
-
-
wait等待第一个终止的子进程。而waitpid则可以指定等待特定的子进程。waitpid提供了一个wait的非阻塞版本。有时希望取得一个子进程的状态,但不想使父进程阻塞,waitpid提供了一个这样的选项:WNOHANG,它可以使调用者不阻塞,如果没有任何一个子进程的进程调用wait函数,他会立即出错返回。
获得进程ID(getpid)
- 原型:
#include<sys/types.h>
#include<unistd.h>
pid_t getpid(void);
setuid和setgid
-
作用:
- setuid:设置实际用户ID和有效用户ID
- setgid:设置实际组ID和有效组ID
-
原型:
#include<sys/types.h>
#include<unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
- 设置用户ID的setuid函数必须遵守以下规则(设置组ID的setgid函数一样):
- 若进程有root权限,则函数将实际用户ID,有效用户ID设置为参数uid。
- 若进程不具有root权限,但uid等于实际用户ID,则setuid只将有效用户ID设置为uid,不改变实际用户ID。
- 若以上两条都不能满足,则函数调用失败,返回-1,并设置errno为EPERM
- 说明:
- 只有超级用户进程才可以改变实际用户ID。所以一个非特权用户进程是不能通过setuid或seteuid得到特权用户权限的。但是su命令能使一个普通用户变成特权用户。
- 因为su是一个“set_uid"程序。执行一个设置了set_uid位的程序时,内核将进程的有效用户ID设置为文件属主的ID。而内核检查一个进程是否有权限访问某文件的权限时,是使用进程的有效用户ID来进行检查的。su程序的文件属主是root,普通用户寻星su命令时,su进程的权限是root权限。
- 当进程的有效用户ID即euid是root用户时,如果调用setuid函数使euid为其他非root用户,则该进程从此进步具有超级用户的权限了。
- 说明:
- 可以这样使用setuid函数:开始时某个程序需要root权限完成一些工作,但后续的工作不需要root权限,可以将该程序执行文件设置为set_uid,并使得该文件的属主是root。这样普通用户执行这个程序时,进程就具有root权限了。当不再需要root权限时,调用setuid(getuid())恢复进程的实际用户ID和有效用户ID为执行该程序的普通用户的ID。对于一些提供网络服务的程序,这样做是非常有必要的。
改变进程的优先级
-
nice
- 通过系统调用nice可以改变进程的优先级。
- 函数原型:
#include<unistd.h> int nice(int increment);
-
getpriority:
- 函数声明:
#include<sys/resource.h> int getpriority(int which,int who);
-
作用:该函数返回一组进程的优先级。
-
参数:which和who确定返回哪一组进程的优先级。
-
which的取值以及who的对应含义
-
函数如果调用成功返回指定进程的优先级,出错返回-1,并设置errno的值
PRIO_PROCESS:一个特定的进程,此时who的取值为进程ID PRIO_PGRP:一个进程组的所有进程,who:进程组ID PRIO_USER:一个用户拥有的所有进程,who:实际用户ID
-
errno:
errno 含义 ESRCH which和who的组合与现存的所有进程都不匹配 EINVAL which是一个无效值 -
-
注意:当指定的一组进程的优先级不同时,getpriority将返回其中优先级最低的一个。当它返回-1时,可能是发生错误,也有可能是返回的是指定进程的优先级。区分它们的唯一方法是:在调用前将errno清零。如果函数返回-1,并且errno不为0,说明有错误产生。
-
setpriority:
- 原型:
#include<sys/resource.h> int setpriority(int which,int who,int prio);
- 作用:该函数用来设置指定进程的优先级。进程指定的方式与上述相同。
- 调用成功返回指定进程的优先级,出错返回-1,并设置相应的errno。除了上面的errno的可能值外,还有下面的:
errno 含义 EPERM 要设置优先级的进程与当前进程不属于同一个用户,并且当前进程没有CAP_SYS_NICE特许 EACCES 该调用可能降低进程的优先级并且没有CAP_SYS_NICE特许 -
通过以上两个函数完全可以改变进程的优先级。nice系统调用是他们的一种组合形式,nice系统调用等价于:
int nice(int increment) { int oldprogetpriority(PRIO_PROCESS,getpid()); return setpriority(PRIO_PROCESS,getpid(),oldpro(increment)); }