进程定义
网上的定义众说纷纭,但都大同小异,就我的理解而言,进程是运行起来的程序(指令),同时还用于内存和地址空间等资源。
进程特征
进程控制块PCB
Linux的调度和资源管理单位,包含了进程的描述信息、控制信息以及资源信息,它是进程的一个静态描述,系统通过PCB来里的信息来进行进程控制,task_struct定义在include/linux/sched.h中
进程标识
实际上就是PID,用以唯一的标识进程,在进程控制代码段中获得当前进程的PID和PPID的系统调用函数为getpid()和getppid(),这样就可以根据PID来找到进程。
进程状态
执行态、阻塞态、就绪态
Linux下的进程
一个进程由数据段、代码段、堆栈段。
数据段:存放程序中的变量、常数、数据空间等等
代码段:是一条条机器指令的集合
堆栈段:存放子程序的返回地址、子程序的参数以及程序的局部变量等等
进程管理
包括对进程的中断操作、改变优先级、查看进程状态等
启动
人工启动:用户键入指令或者手动设置一个后台运行的脚步等等
调度启动:指定任务运行的时间或者场合,到时候系统就会自动完成这一切工作
常用进程控制命令
Linux进程编程
fork()
介绍:
作用是创建新进程,一次执行会返回两个值
在进程A中调用fork函数会创建一个子进程B,进程B是进程A的复制品,继承了整个进程的地址空间,包括进程上下文、代码段、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等。
当代码从fork调用返回后,我们会得到一个返回值,如果返回值为0,则当前进程为子进程B,否则为进程A,A得到的返回值为B的PID。在进程执行过程中,往往根据fork返回值来确定当前运行在进程A还是进程B以执行不同功能。
fork()函数的系统开销比较大,而且执行速度也不是很快
注意:
每使用一次fork函数,则创建一个新进程,不要多次使用。
有些UNIX系统设计者创建了vfork()。vfork()也能创建新进程,但它不产生父进程的副本。它是通过允许父子进程可访问相同物理内存从而伪装了对进程地址空间的真实拷贝,当子进程需要改变内存中数据时才复制父进程。
exec()
介绍:
与fork函数不同,exec是释放自己,让另一个不同的进程替代自己:在进程中启动另一个程序执行的方法。
根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换。
如果一个进程想执行另一个程序,那么它就可以调用fork()函数新建一个进程,然后调用exec函数族中的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程(相当普遍)
exec函数族
Linux中没有直接的exec函数,只有以其为前缀的函数,语法有细微差别
具体差别如何,这里推荐一篇文章
exit()
终止进程。执行到exit()或者_exit()时,进程会无条件地停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。
exit()与_exit()的区别:
就在于exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区中的内容写回文件
wait()
阻塞进程,直到一个子进程结束或者接收到指定信号。
waitpid()和wait()一样,可提供一个非阻塞版本的wait()功能,也能支持作业控制。实际上wait()函数只是waitpid()函数的一个特例,在Linux内部实现wait()函数时直接调用的就是waitpid()函数
对于waitpid这里推荐一篇文章
守护进程Daemon
介绍:
一种后台服务进程,往往用于周期性地执行某种任务或等待处理某些发生的事件。例如,作业规划进程crond、打印进程lqd等(这里的结尾字母d就是Daemon的意思)
Linux中,从某一终端启动的进程往往依赖于该终端,当终端关闭,进程也自动关闭,但守护进程不是,当系统关闭后才会结束。
如果想让某个进程不因为用户、终端或者其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。
编写:
- 创建子进程,父进程退出
所有工作都在子进程中完成,而用户在shell终端里则可以执行其他的命令,从而在形式上做到了与控制终端的脱离。
由于父进程已经先于子进程退出,会造成子进程没有父进程,从而变成一个孤儿进程。在Linux中,每当系统发现一个孤儿进程,就会自动由1号进程(也就是init进程)收养它,这样,原先的子进程就会变成init进程的子进程了。
关键代码:
pid = fork();
if(pid > 0) exit(0);
....
2. 在子进程中创建新会话
前置知识
进程组:一个或多个进程的集合,每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID。且该进程ID不会因组长进程的退出而受到影响。
会话期:一个或多个进程组的集合,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期
setsid():用于创建一个新的会话,并担任该会话组的组长。
- 让进程摆脱原会话的控制
- 让进程摆脱原进程组的控制
- 让进程摆脱原控制终端的控制。
为什么要调用setsid()函数?
fork()函数创建子进程再令父进程退出,子进程全盘复制了父进程的会话期、进程组和控制终端等,虽然父进程退出了,但原先的会话期、进程组和控制终端等并没有改变,因此,还不是真正意义上的独立,而setsid()函数能够使进程完全独立出来,从而脱离所有其他进程的控制。
3.改变目录
fork()创建的子进程继承了父进程的当前工作目录,文件系统(比如“/mnt/usb”等)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让“/”作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径。
方法是使用chdir函数。
4.重设文件权限掩码
由于使用fork()函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask()。在这里,通常的使用方法为umask(0)。
关于什么是文件权限掩码:
5.关闭文件描述符
fork()函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法被卸载。
在上面的第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf())输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2的3个文件(常说的输入、输出和报错这3个文件)已经失去了存在的价值,也应被关闭。通常按如下方式关闭文件描述符:
for(i=0;i<MAXFILE;i++)
close(i);
出错:
由于守护进程完全脱离了控制终端,因此,不能像其他普通进程一样将错误信息输出到控制终端来通知程序员,即使使用gdb也无法正常调试。
通用的办法是使用syslog服务,将程序中的出错信息输入到系统日志文件中。
具体如何操作,我个人认为属于优化,不用介绍。
该机制提供了3个syslog相关函数,分别为openlog()、syslog()和closelog()。