Linux 进程

来源:互联网 发布:python安卓开发 编辑:程序博客网 时间:2024/05/16 07:27
进程在Linux系统中,其作用相当于文件的抽象概念;
进程都由一个唯一的标识符表示,即进程ID,简称pid;系统保证在某时刻每个pid都是唯一的。
空闲进程是当没有其他进程在运行时,内核所运行的进程,他的pid=0;系统启动后的第一个进程是init,pid=1;除非用户显示的指定内核要运行的程序,否则内核就必须寻找一个适合的init程序,内核会顺序进行:
1、/sbin/init:    init最有可能存在的地方
2、/etc/init:    另一个可能的地方
3、/bin/init:    init一个可能存在的位置
4、/bin/sh:    Bourne shell的所在的位置,内核没有找到init时,内核就会运行它;
如果在上面没有发现init,那么系统就会挂起;

进程ID 的分配:
默认情况下,内核将进程ID的最大值限制为32768,如果要改变这个值,可以设置/proc/sys/kernel/pid_max的值就可以了;内核分配进程ID是以严格的线性函数方式进行的,比如17是当前进程的,那么16就会分配给新进程,当值达到pid_max之前,是不会重复的;

进程的体系:
创建新进程的那个进程为父进程,而新进程是子进程。其中在子进程中的ppid中保存着父进程ID。每个进程都被一个用户和组所拥有,这是用来实现访问控制的,对于内核来说,用户和组仅仅是一些整数值,通过/etc/passwd和/etc/group两个文件进行映射。每个子进程都继承了父进程的用户和组;

pid_t:是进程的ID结构,是无符号类型:
他定义在<sys/types.h>中;
获取进程ID和父进程ID:
#include<sys/types.h>#include<unistd.h>Pid_t getpid(void);//返回调用进程ID//pid_t getppid(void);//返回调用进程的父进程ID//

运行新的进程:
在Unix中,载入内存并执行程序映像的操作与创建一个新进程的操作是分离的。Unix有一个系统调用是可以将二进制文件的程序映像载如内存,替换原先进程的地址空间,并开始运行它,这就是运行一个新的程序,而相应的系统调用称为exec系统调用。
同时另一个不同系统调用是创建一个新的进程,他基本就是复制父进程,完成创建新进程的这种行为叫做派生(fork),完成这个功能的系统调用就是fork(),这两种操作首先fork(),即创建新的进程;然后运行,即载入镜像文件,都要求在新的进程中运行新的程序。也就是先创建【复制】在替换。

exec系列的系统调用:
没有单一的exec系统调用,它们由基于单个系统调用的一组exec函数构成,execl():
#include<unistd.h>
int execl(const char *path,const char *arg,...);
他的调用将path所指路径的映像载入内存,替换当前的进程映像,arg是他的【进程的】第一个参数,参数是可变的,但是必须是以NULL结尾;
如:
int ret=execl("/bin/vi","vi",NULL);//在次应该遵循Unix的习俗,用vi作为第一个参数//
该调用的作用是载入/bin/vi程序的二进制替换当前进程;例如如果你想编辑/home/kidd/hooks.txt那么可以使用下面的调用:
execl("/bin/vim","vi","/home/kidd/hooks.txt",NULL);
execl成功调用不仅仅改变了地址空间和进程的映像,还改变了进程的一些属性:任何挂起的信号都会丢失;捕捉的任何信号还原默认;任何内存锁定会丢失;多数线程的属性会还原到缺省值;进程的统计信息会复位;与进程相关的数据会丢失,包括映射文件;

#include<unistd.h>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 *constargv[]);int execvp(const char *file,char *const argv[]);int execve(const char *filename,char *const argv[],char *const envp[]);


l and v分别表示参数是以列表方式或者数组方式提供的;
p意味着在用户的PATH环境变量中寻找可执行文件;
如:
const char *args[]={"vi","/home/kidd/books.txt",NULL};
execvp("vi",args);
由此可以看出,在使用execX时,是用于启动另一进程替换现有进程;
如:execl("/bin/ls","vi",NULL);//该语句的作用是执行ls程序,用来替换现执行的进程,那么执行此语句将显示目录//
其作用是很方便的,有时可以用来启动控制台程序//


fork()系统调用:
创建一个和当前进程映像一样的进程可以通过fork()系统调用。
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
成功调用fork()会创建一个进程,它几乎与调用fork()的进程一模一样。父进程与子进程的区别:子进程的pid是新的,与父进程的pid是不同的;子进程的ppid会设置为父进程的pid;子进程中的资源统计信息会清零;任何挂起的信号会清零,也不会被子进程继承;任何文件锁都不会被子进程继承;
最常见的fork用法是创建一个新的进程,然后使用exec系列来载入二进制映像;

写时复制:
早期的Unix系统中,创建进程比较原始,调用fork时,内核会把所有的内部元素结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。逐页复制是很费时的工作,所有在linux中使用了写时复制的方法,而不是对父进程空间进行整体复制。写时复制是一种采取了惰性优化方式来避免复制时的系统开销。
Vfork调用:
#include<sys/types.h>#include<unistd.h>pid_t vfork(void);


对vfork的成功调用所产生的结果和fork是一样的,他会挂起父进程直到子进程终止或者运行了一个新的可执行文件映像【也就是会等待子进程结束】,vfork避免了地址空间的按页复制。父进程和子进程共享相同的地址空间和页表项,他就是完成了一件事:复制内部的内核数据结构。与fork()相反的是,当父进程创建了子进程以后,就会立即返回。

终止进程:
#include<stdlib.h>
void exit(int status);
对它的调用通常会执行一些基本的终止进程步骤,然后通知内核终止这个进程。这函数从不返回,status用来标识进程推出的状态,值为:EXIT_SUCCESS  and  EXIT_FAILURE;
在终止进程前,C函数执行以下关闭进程:
以在系统中注册的逆序来调用由atexit()  or  on_exit()注册的函数;
空所有已打开的标准I/O流;
删除由tmpfile()创建的所有临时文件;
当进程退出时,内核会清理进程所创建的、不在使用的任何资源,这包括:申请的内存、打开的文件、systemV的信号量。清理完后,内核摧毁进程,并告知父进程其子进程的终止。当然应用程序可以直接调用_exit(),但这通常是不合适的,许多的应用程序需要做一些完全退出过程中所需要的清理工作,如情空:stdout流。notice:vfork()的使用者终止进程时必须使用_exit(),而不是exit();

其他终止进程的方式:
终止进程的典型方式不是通过明确的使用一个系统调用,而是采用跳转到程序结尾处的方式。也就是main函数返回时,但是这中方式是,编译器还是会在最后代码中插入一个_exit()。在main返回时调用exit()是一中编程的号习惯。
最后一种是进程被内核惩罚性终止,内核就会杀死执行非法指令,引起一段错误,或者内存耗尽的进程。

atexit():
用来注册一些在进程结束时要调用的函数,也就是可以用来在进程退出以后调用某个函数,这在之前只是听说了一次:
#include<stdlib.h>int atexit(void (*function)(void));


对atexit()的成功调用会把指定的函数注册到进程正常结束时调用的函数中;如果进程调用了exec,所有注册的函数列表会被清空,如果进程是通过信号而结束的,这写注册的函数也不会被调用。要注册的函数必须是无参数的,且也没有返回值,如:void function(void);
函数调用的顺序是和注册的顺序相反的,也就是这些函数村村在栈中。注册的函数不能调用exit(),否则会引起无限的递归调用。且在注册函数的调用过程中,其调用顺序是倒序调用的。

on_exit():作用是和atexit一样的,只是注册的方式不同:
#include<stdlib.h>
int on_exit(void (*function)(int,void *),void *arg);
注册函数原型是:void my_function(int status,void *arg);可以看出arg和注册函数的是一样的,在注册时,必须保证arg所指的内存地址必须是有效的。

等待特定进程:
检测子进程的行为是很重要的,通常一个进程可能有很多子进程,但不需要等待所有子进程的结束,父进程只想等待其中一个特定的子进程。一种解决方法是多次调用wait(),每次根据返回值来判断是不是那个特定的进程,父进程必须保存所有wait()的返回值,以备将来会用到,如果知道等待进程的pid,可以使用waitpid系统调用:
#include<sys/types.h>#include<sys/wait.h>pid_t waitpid(pid_t pid,int *status,int options);


他是一个更强大的系统调用,额外参数可以用来微调;pid是需要等待的一个或多个进程Pid,必须是:<-1[等待所有在组ID的进程,其ID是他的绝对值]、-1[等待任意子进程]、0[等待与调用进程处于同一进程组的任一进程]、>0[等待传入的pid的进程];status和wait是一样的,用来保存主状态;options是零个或多个下面选项的二进制“或”运算,也就是调用等待的状态:WNOHANG or WUNTRACED  or WCONTINUED  so on;

其他等待子进程的方法:
作为应用程序来说,他们希望有更多的等待子进程的方式,所有下面还提供了等待的方法:
#include<sys/wait.h>
int waitid(idtype_t idtype,id_t id,siginfo_t *infop,int options);
我们指定使用vfork创建的进程是当子进程结束了才返回;
和wait与waitpid一样,waitid的作用是等待子进程的结束和了解子进程状态改变的信息,它有更多的选择,但是确增加了复杂度。
他允许程序员指定所要等待的子进程。idtype和id用来指定所要等待的子进程,idtype的值是下面三个中的一个:P_PID【等待pid值是id的子进程】、P_GID【等待进程组ID是id那些子进程】、P_ALL【等待所有的子进程,id被忽略】;参数ID是很少见的id_t类型,这种类型代表着一种通用的ID号,它的类型是足够大的,保证了可以保存任何类型的pid_t值,有时可以把他当作pid_t来使用。参数options是以下一个或者多个选项进行二进制“或”运算的结果:WEXITED【调用进程会等待结束的子进程】、WSTOPPED【等待收到信号而停止的子进程】、WCONTINUED【会等待收到信号而继续执行的子进程】、WNOHANG【调用不阻塞】、WNOWAIT【调用进程不会移除满足条件的子进程的僵死状态】;成功调用时,会填充infop,但是它必须指定一个有效的siginfo_t类型;


创建并等待一个新进程:POSIX都定义了一个用于创建新进程并等待它结束的函数----可以想想的是同步的创建进程。如果一个进程创建了新的进程然后立刻开始等待它,可以使用:
#include<stdlib.h>
int  system(const char *command);
system之所以这样命名是因为进程同步创建一般被称为“交付给系统运行”,使用system来运行一个简单的工具或者shell是很常见的,大多数都是希望得到工具或脚本的返回值。对system()调用会使得参数command指定的程序得到执行,且程序可以得到相应的参数,“/bin/sh -c”会作为前缀加到command参数前面,这样才会将参数传递给shell。意思就是使用system来运行另一个程序,并且还会得到返回值。函数成功时,返回值是执行命令得到的返回状态,如同wait的返回值一样。执行命令的返回值会通过WEXITSTATUS得到,调用失败时返回-1;如果command是NULL,且/bin/sh是可用的,返回一个非0的值,其他情况返回0;当然可以使用fork、execv、waitpid实现system相同的功能,如:
int my_system(const char *cmd){    int status;    pid_t pid;    pid=fork();    if(pid==-1)  return -1;    else if(pid==0){    const char *argv[4];    argv[0]="sh";    arv[1]="-c";    argv[2]=cmd;    argv[3]=NULL;    execv("/bin/sh",argv);    return 0;   }   if(waitpid(pid,&status,)==-1)  return -1;   else if(WIFEXITED(status))  return WEXITSTATUS(status);   return 0;}




僵死进程:
一个进程已经终止了,但是它的父进程还在等待获得它的状态,那么这个进程就叫僵死进程,僵死进程还会消耗一些资源,尽管这些资源很多少,仅仅能描述进程曾经的状态。除非得到了想要到的信息,否进程就一直等待下去。如果得到了资源,内核就会清除这些信息,僵死就不存在了。
无论何时,只要有进程结束了,内核就会遍历它的所有子进程,并且把它们的父进程重新设为init进程,这保证了系统中不存在没有父进程的进程,这就是说当某一进程的父进程结束以后,如果子进程还在运行着,那么系统就会把他的父进程设置为系统进程init,也就是pid为1;

用户和组:
进程是与用户和组关联的。用户ID和组ID分别用uid_t  and  gid_t表示;数字表示和可读字符串之间的映射关系是通过用户空间的/etc/passwd和/etc/group两个文件完成的,内核值处理数字表示的。在linux系统中,一个进程的用户ID和组ID代表这个进程可以执行哪些操作。可以使用命令ls -l查看文件的详细信息,就同时可以查看权限了,有rwx三中权限。

实际用户ID、有效用户ID、保存设置的用户ID:
与进程相关的用户ID有四个而不是一个,它们是:实际用户ID【运行当前进程的用户uid,并且会被设置为父进程的实际用户ID】、有效用户ID【是当前进程所使用的用户ID】、保存设置用户ID【是原先进程的有效ID】和文件系统用户ID。当然这写都是可以改变的,可以查看其相关内容。同样的可以使用:uid_t getuid(void)  and  gid_t getgid(void)获得相应的ID;

会话和进程组:
每个进程都属于某个进程组,进程组是由一个或多个相互间有关联的进程组成的,他的目的是为了进行作业控制,进程组的主要特征就是信号可以发送给进程组中的所有进程:这个信号可以使同一个进程组中的所有进程终止、停止或者继续运行。
每个进程组都由进程组ID唯一的标识,并且有一个组长进程。进程组ID就是组长进程的ID,只要进程组中还有一个进程存在,那么这个进程组就存在。即使组长进程终止了,该组依然存在。
当有新的用户登录计算机,登录进程就会为这个用户创建一个会话,这会话只有用户登录的shell一个进程,shell作为会话首进程。其中的会话进程pid就作为会话的ID,一个会话就是一个或多个进程组的集合,会话包括了登录用户的所有活动,并分配一个control terminal,control terminal是一个用户I/O的tty设备。
进程组提供了向其中的所有进程发送信号的机制,值让作业控制和其他的shell功能变得很容易,同时的会话将登录与控制终端联系起来。进程组被分为一个前台进程组和0个或者多的后太进程组,当用户退出终端时,像前台的所有进程发送SIGQUIT信号,当出现网络中断时,像前台进程组的所有进程发送SIGHUP信号,当敲击ctrl+c时,像前台所有的进程发送SIGNT信号。

守护进程:
守护进程运行后台,不与任何控制终端相关联,守护进程通常是在系统启动时就运行了,用于处理系统级任务;
守护进程有两个要求:它必须是init进程的子进程,并且不与任何控制终端相关联,可以通过以下成为守护进程:
1、调用fork,创建新进程,它会是将来的守护进程;
2、在守护进程的父进程中调用exit(),
3、调用setid(),使得守护进程有一个新的进程和新的会话,两者都把它作为首进程。
4、关闭所有的文件描述符;
5、打开0、1、2号文件描述符,把它重定向到/dev/null;











原创粉丝点击