一、进程概述
1、程序,进程及线程间的区别与联系
程序是指一组指示计算机或其他具有消息处理能力装置每一步动作的指令,通常用某种程序设计语言编写,运行于某种目标体系结构上。
进程,是计算机中已运行程序的实体。进程为曾经是分时系统的基本运作单位。在面向进程设计的系统(如早期的UNIX,Linux 2.4及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。
(进程是动态的,程序是静态的;进程是运行中的程序,程序是保存在硬盘上的可执行的代码。)
程序转化成进程
1、内核将程序读入内存,为程序分配内存空间
2、内核为该进程分配进程标识符(PID)和其他所需资源
3、内核为该进程保存PID及相应的状态信息,把进程放到运行队列中等待执行。
当程序转化为进程后就可以被操作系统的调度程序调度执行了。
线程,是操作系统能够进行运算调度的最小单位,线程在进程内部,线程与同属一个进程的其他线程共享进程拥有的全部资源。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程可以并行执行。
2、进程标识
pid_t_getpid(id) 获得进程ID
pid_t_getppid(id) 获得进程父进程ID
pid_t_getuid(id) 获得进程的实际用户ID
pid_t_geteuid(id) 获得进程的有效用户ID
pid_t_getgid(id) 获得进程的实际组ID
pid_t_getepid(id) 获得进程的有效组ID
用户与用户组的相关概念:
1、实际用户ID和实际用户组ID:标识执行程序的是谁。也就是登录用户的uid和gid,比如我的Linux以simon登录,在Linux运行的所有的命令的实际用户ID都是simon的uid,实际用户组ID都是simon的gid(可以用id命令查看)。
2、有效用户ID和有效用户组ID:进程用来决定我们对资源的访问权限。
一般情况下,有效用户ID等于实际用户ID,有效用户组ID等于实际用户组ID。当设置-用户-ID(SUID)位设置,则有效用户ID等于文件的所有者的uid,而不是实际用户ID;同样,如果设置了设置-用户组-ID(SGID)位,则有效用户组ID等于文件所有者的gid,而不是实际用户组ID。
4、进程的结构
包括代码段,数据段,堆栈段
代码段:存放程序的可执行代码
数据段:存放程序的全局变量,常量和静态变量
堆栈段:栈用于函数调用,放着函数的参数,函数内部定义的局部变量
5、进程的内存映像
从低地址到高地址:
代码段:二进制机器代码,只读可共享。某进程创建一个子进程,父子进程共享代码段,子进程还获得父进程的数据段、堆、栈的复制。
数据段:存储已被初始化的变量,包括全局变量和已被初始化的静态变量。
未初始化的数据段:存储未被初始化的静态变量,它也被称为bss段。
堆:用于存放程序运行中动态分配的变量。
栈:用于函数调用,保存函数的返回地址、函数的参数、函数内部定义的局部变量。
高地址中还存储了命令行参数和环境变量。
内存映像位于内存中,随着程序的执行在动态变化。
二、进程操作
1、创建
fork 函数
#include <unistd.h>
#include <sys/types.h>
pid_t fork(void);
若进程创建成功,会有两次返回值。一个是子进程中fork函数的返回值,该返回值是0;另一个是父进程调用fork函数的返回值,该返回值,是刚刚创建的子进程的ID。
创建失败仅返回-1,可能因为父进程拥有的子进程的个数超过规定的限制(EAGAIN),也有可能是可供使用的内存不足(ENOMEN)。
子进程会继承父进程的很多属性,但是还有一些区别。
父子进程有什么区别:
1、子进程有自己唯一的ID
2、fork的返回值不同
3、不同的父进程ID
4、不继承父进程设置的文件锁和警告
5、子进程的未决信号集会被清空
6、虽然共享父进程打开的文件标识符,但是父进程中文件标识符的改变不会影响子进程的标识符。
为什么会有两次返回值?
所有函数的返回值是存储在寄存器eax中的,当fork返回时,子进程会返回0是因为在初始化任务段时TSS,将eax值置为0。父进程调用copy_process得到lastpid是子进程的pid,所以父进程将lastpid放入eax中,父进程中fork返回子进程pid,子进程切到cpu上执行时,加载上下文环境(加载TSS),寄存器eax的值被置为0,所以子进程中fork的返回值为0。
孤儿进程:父进程先于子进程结束,子进程就成了一个孤儿进程,由init进程收养,成为inti的子进程 。inti进程的PID不一定为1,比如我的电脑中是pid为1564的收养了这个孤儿,和init有一样的功能。
僵尸进程:一个子进程结束了,但是他的父进程没有等待(调用wait / waitpid)他, 那么他将变成一个僵尸进程。(父进程没有结束,且不知道子进程已经结束)
vfork函数
跟fork一样,调用一次,返回两次。
子进程共享父进程的空间,子进程完全运行在父进程的地址空间上,子进程对该地址空间中任何数据的修改同样为父进程可见。vfork保证子进程先运行,当它调用exec或exit后,父进程才可能被调度运行。如果在exit或者exec之前要依赖父进程,那么会导致死锁。
可以减小系统开销,但是用vfork时一定要谨慎。
2、创建守护进程
1. 在进程在后台运行。
为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。
if(pid=fork()) exit(0);//是父进程,结束父进程,子进程继续
2. 脱离控制终端,登录会话和进程组 setsid();
说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
3. 禁止进程重新打开控制终端
现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:
if(pid=fork()) exit(0);//结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
4. 关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们: for(i=0;i 关闭打开的文件描述符close(i);>
5. 改变当前工作目录
进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir(“/”)
6. 重设文件创建掩模
进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);
7. 处理SIGCHLD信号
处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。
signal(SIGCHLD,SIG_IGN);
这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。
3、进程退出
正常退出:在main函数中执行return,调用exit,_exit
异常退出:调用abort函数 ; 进程收到某个信号,该信号使进程终止
exit与return的区别:
exit函数在头文件stdlib.h中。
exit(0):正常运行程序并退出程序;exit(1):非正常运行导致退出程序;
return():返回函数,若在main主函数中,则会退出函数并返回一值。
详细说:
1. return返回函数值,是关键字;exit是一个函数。
2. return是语言级别的,它表示了调用堆栈的返回;而exit是系统调用级别的,它表示了一个进程的结束。
3. return是函数的退出(返回);exit是进程的退出。
4. return是C语言提供的,exit是操作系统提供的(或者函数库中给出的)。
5. return用于结束一个函数的执行,将函数的执行信息传出个其他调用函数使用;exit函数是退出应用程序,删除进程使用的内存空间,并将应用程序的一 个状态返回给OS,这个状态标识了应用程序的一些运行信息,这个信息和机器和操作系统有关,一般是 0 为正常退出,非0 为非正常退出。
6. 非主函数中调用return和exit效果很明显,但是在main函数中调用return和exit的现象就很模糊,多数情况下现象都是一致的。
return会释放局部变量,并弹栈,回到上级函数执行。exit直接退掉。如果你用c++ 你就知道,return会调用局部对象的析构函数,exit不会。(注:exit不是系统调用,是glibc对系统调用 _exit()或_exitgroup()的封装)。
exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是”清理I/O缓冲“。
4、执行新程序
#include <unistd.h>
extern char **environ;
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 execve(const char *file,char *const argv[],char *const envp[]);
只有excve()是系统调用,其余都是库函数
这6个函数在函数名和使用语法的规则上都有细微的区别,下面就从可执行文件查找方式、参数传递方式和环境变量这几个方面进行比较。
● 查找方式:两个函数(也就是以 p 结尾的两个函数)可以只给出文件名,其它四个要给出完整的路径名,系统就会自动按照环境变量“$PATH” 所指定的路径进行查找。
● 参数传递方式:exec函数族的参数传递有两种:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。在这里是以函数名的第5位字母来区分的,字母为 “l”(list)的表示逐个列举参数的方式,其语法为const char *arg;字母为“v”(vector)的表示将所有参数整体构造指针数组传递,其语法为 char *const argv[]。这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身)。要注意的是,这些参数必须以NULL结束。
● 环境变量: exec函数族可以默认系统的环境变量,也可以传入指定的环境变量。这里以 “e”(environment)结尾的两个函数 execle()和 execve()就可以在 envp[]中指定当前进程所使用的环境变量。
main函数的完整形式
int main(int argc,char *argv[],char ** envp)
argv和envp的大小都是受限制的
通过宏ARG_MAX来限制,大小为131072(linux/limits.h)
5、等待进程结束
wait函数的原型为:pid_t wait(int *status)
当进程退出时,它向父进程发送一个SIGCHLD信号,默认情况下总是忽略SIGCHLD信号,此时进程状态一直保留在内存中,直到父进程使用wait函数收集状态信息,才会清空这些信息.
用wait来等待一个子进程终止运行称为回收进程.
wait()的参数
如果参数的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,
由于这些信息被存放在一个整数的不同二进制位中,所以就设计了一套专门的宏来完成这项工作,其中最常用的两个:
1,WIFEXITED(status)这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值.
2,WEXITSTATUS(status)当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;
如果子进程调用exit(7),WEXITSTATUS(status)就会返回7.请注意,如果进程不是正常退出的,也就是说,WIFEXITED返回0,这个值就毫无意义.
waitpid函数的原型为 pid_t waitpid(pid_t pid,int *status,int options)
从本质上讲,系统调用waitpid是wait的封装,waitpid只是多出了两个可由用户控制的参数pid和options,为编程提供了灵活性.
waitpid的参数说明:
参数pid的值有以下几种类型:
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去.
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样.
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬.
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值.
参数options的值有以下几种类型:
如果使用了WNOHANG参数,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去,非阻塞版本.
如果使用了WUNTRACED参数,则子进程进入暂停则马上返回,但结束状态不予以理会.
Linux中只支持WNOHANG和WUNTRACED两个选项,这是两个常数,可以用”|”运算符把它们连接起来使用,如果我们不想使用它们,也可以把options设为0,如:ret=waitpid(-1,NULL,0);
waitpid的返回值比wait稍微复杂一些,一共有3种情况:
a、当正常返回的时候waitpid返回收集到的子进程的进程ID;
b、设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
c、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD.
三、进程的其它操作
1、获得进程ID
使用getpid
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
2、setuid和setgid
SUID权限仅对二进制有效,执行者对该程序要有可执行权限,本权限仅在执行过程中有效,此时执行者具有该程序所有者的权限。
SGID类似,在执行过程中获得该用户组的支持
SBIT仅对目录有效,对目录要有WX权限,用户在该目录下创建文件或者目录,仅有自己和root才能删除
设置用户ID的setuid函数遵守的规则(setgid类似)
若进程有root权限,则函数将实际用户ID、有效用户ID设置为参数uid
若进程不具有root权限,但uid等于实际用户ID,则setuid只将有效用户ID设置为uid,不改变实际用户uid
若两个条件都不满足;则函数调用失败,返回-1,并设置errno为EPERM
3、改变进程的优先级
Linux实际上实现了140个优先级范围,取值范围是从0-139,这个值越小,优先级越高。nice值的-20到19,映射到实际的优先级范围是100-139。所有优先级值在0-99范围内的,都是实时进程,所以这个优先级范围也可以叫做实时进程优先级,而100-139范围内的是非实时进程。
越nice的人抢占资源的能力就越差,而越不nice的人抢占能力就越强。这就是nice值大小的含义,nice值越低,说明进程越不nice,抢占cpu的能力就越强,优先级就越高。
#include<unistd.h>
int nice(int increment)
进程的优先级一般从父进程继承,一般为0。