进程是系统资源的最小单元,很重要。
7.1 linux进程的基本概念
- 定义:一个程序的一次执行过程,同时也是资源分配的最小单元。程序是静态的,而进程是动态的。
- 进程控制块:linux系统用进程控制块描述进程,task_struct,在 include/linux/sched.h
- PID,进程唯一标识;PPID,父进程的PID
#include <unistd.h> /* Get the process ID of the calling process. */
extern __pid_t getpid (void) __THROW; /* Get the process ID of the calling process's parent. */
extern __pid_t getppid (void) __THROW;
进程相关的还有用户和用户组标识、进程时间、资源利用的函数,参考APUE.
- 进程运行的状态
- 进程的结构:主要包含数据段、代码段、堆栈段
- 进程模型:用户态和内核态
- linux启动进程的两种方式:
- 手动启动:前台启动,最常见的是在终端里输入命令,该命令的执行就是一个进程; 后台启动,用&,不影响终端,在终端后面默默运行。
- 调度启动:制定时间运行,有一些命令,at命令可以在指定时刻执行相关进程;cron命令可以自动周期性的执行相关进程。
常用进程相关命令:
7.2 linux进程编程
7.2.1 fork
- 从已创建的进程中创建一个新的进程,新进程叫子进程,原进程叫父进程。
- 子进程是父进程的复制,集成父进程的绝大部分内容,包括:整个进程的地址空间、进程上下文、代码段、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等;
- 子进程独有的部分:进程号、资源使用、计时器等
- fork的大体流程:父进程执行fork——>父进程复制出一个子进程——>父子进程从fork函数返回开始分别在两个地址空间同时运行,通过返回值区分父子进程。
- fork的开销比较大,复制这么多东西,想想都觉得累。有些unix系统创建了vfork()函数,vfork创建新进程时,不产生父进程的副本,允许父子进程访问相同的物理内存而伪装成拷贝父进程。但是子进程需要改变内存时(写),才复制父进程,这就是“写时复制”,linux的fork就是调用vfork函数实现的。
#include <sys/types> // pid_t
#include <unistd.h> pid_t fork( void );
参数:
返回值:
0:子进程
>0:子进程pid,父进程
-1:出错
注意事项:
fork调用一次,就创建一个子进程,所以if、else if等分支处理时,不能多次调用,应该调用一次,记下返回值,然后if else等使用此返回值。
/* 7-1,fork */ #include <stdio.h> // printf
#include <stdlib.h> // exit
#include <unistd.h>
#include <fcntl.h> // open,fcntl
#include <sys/types.h> int main(int args, char *argv[])
{
pid_t pid_rtn; pid_rtn = fork();
if( pid_rtn == )
{
printf("\r\nChild thread, pid %d, ppid %d",getpid(),getppid());
}
else if( pid_rtn > )
{
sleep(); // 如果父进程先结束,则子进程会被init进程收养,用getppid时获取的就不是创建他的父进程了
printf("\r\nParent thread, pid %d, child %d",getpid(),pid_rtn);
}
else
{
printf("\r\nfork err.");
} printf("\r\nfinish.\r\n");
exit();
} $ ./example
Child thread, pid 4087, ppid 4086
finish. Parent thread, pid 4086, child 4087
finish. 如果父进程不睡1s,则运行结果如下:
$ ./example Parent thread, pid 4121, child 4122
finish.
$
Child thread, pid 4122, ppid
finish. $ ps -A
*
2326 ? 00:00:01 upstart // upstart就是ubuntu的init进程,对于父进程已经结束的子进程,会被这个进程“收养”
*
7.2.2 exec函数族
- 执行另一个程序,除了pid外,其他全被新的进程替换
- 一般先fork,然后exec执行想执行的程序
- exec注意事项:一定要加上错误判断语句,exec很容易出错,常见错误有:
- 找不到文件或路径,errno=ENOENT
- argv和envp忘记用NULL结束,errno=EFAULT;
- 没有对应可执行文件的运行权限,errno=EACCES
- 6个函数中,真正的系统调用只有execve,其他都是库函数,通过调用execve实现
#include <unistd.h> int execl(const char *path, const char *arg, ...) // list
int execv(const char *path, char *const argv[]) // vector
int execle(const char *path, const char *arg, ..., char *const envp[]) // enviroment
int execve(const char *path, char *const argv[], char *const envp[])
int execlp(const char *file, const char *arg, ...)
int execvp(const char *file, char *const argv[])
参数:
path和file:查找方式,path完整的文件目录路径;file(p结尾的函数)只给出文件名,系统按照环境变量PATH指定的路径查找;
arg...和argv[]:参数传递方式,list和vector,这些参数必须以NULL结尾,以可执行程序命令本身开头;
envp:环境变量,e结尾,指定要执行的进程所使用的环境变量
返回值:-1 出错
/* 7-2,exec */
#include <stdio.h> // printf
#include <stdlib.h> // exit
#include <unistd.h>
#include <fcntl.h> // open,fcntl
#include <sys/types.h>
int main(int args, char *argv[])
{
pid_t pid_rtn;
pid_rtn = fork();
if( pid_rtn == 0 )
{
execlp("ps","ps","-A","NULL"); // 第一个ps是文件名,后面是参数,输入参数时,第一个参数是要运行的程序,跟在shell里输入是一样的,注意要用NULL结尾
}
printf("\r\nfinish.\r\n");
exit(0);
}
相当于执行了“ps -A”命令,运行结果:
PID TTY STAT TIME COMMAND
1 ? Ss 0:12 /sbin/init splash
2 ? S 0:00 [kthreadd]
......
7.2.3 exit和_exit
- 两个函数会停止所有操作,清除PCB等数据结构;
- 两个函数有差别:
- _exit:直接停止运行, 清除进程使用的内存空间,清除内核中的数据结构;
- exit = “清理IO缓存”+_exit, 清理IO缓存,指检查文件的打开情况,把文件缓冲区中的内容写回文件。linux里有“缓冲IO”操作,例如printf、fgets等,使用缓冲区,类似cache。
- 只使用exit()就可以了
进程调用exit()和_exit()后不会立即退出,而是进入僵死zombie状态,变成僵尸进程,僵尸进程只在进程列表里保留一个位置,记录该进程的退出状态等供其他进程收集(一般是父进程用wait收集)。
#include <unistd.h> // _exit
#include <stdlib.h> // exit void exit( int status );
void _exit( int status); 参数:
status 可以返回本进程(调用exit的进程)的退出状态,一般0表示正常,其他数值表示出错,进程非正常结束;
父进程用wait()系统调用接收子进程的返回值。
#include <stdio.h> // printf
#include <stdlib.h> // exit
#include <unistd.h>
int main(int args, char *argv[])
{
printf("Start.\n");
printf("content in buffer.");
_exit(0);
}
$ ./example // 在缓冲区里就没有了,因为_exit不刷缓冲区
Start.
#include <stdio.h> // printf
#include <stdlib.h> // exit
#include <unistd.h>
int main(int args, char *argv[])
{
printf("Start.\n");
printf("content in buffer.");
exit(0);
}
$ ./example // 在缓冲区里的也刷出来了,exit干的
Start.
content in buffer.
【注意】
printf遇到“\n”换行符时自动从缓冲区中将记录读出
7.2.4 wait和waitpid
- wait阻塞等待1个子进程结束,如果该进程在阻塞时接到了一个指定的信号,则阻塞也可能终止。如果没有子进程或者子进程已经结束,则wait会立即返回。
- waitpid比wait功能丰富,可提供非阻塞、作业控制、指定待等待进程等功能
#include <sys/types.h>
#include <sys/wait.h> pid_t wait( int * status );
参数:
status:返回子进程的退出状态和异常终止状态,若为NULL,则不获取。可以通过一些linux特定的宏来测试具体状态信息。
【重要】:进程退出有正常退出(子进程exit或者return),此时的状态记为“正常退出状态”;还有异常退出的情况,例如被信号中断等,这时的状态记为“异常终止状态”。status可以反映这两种状态。
union wait
{
int w_status;
struct
{
# if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int __w_termsig:7; /* Terminating signal. */
unsigned int __w_coredump:1; /* Set if dumped core. */
unsigned int __w_retcode:8; /* Return code if exited normally. */
unsigned int:16;
# endif
} __wait_terminated; // 正常退出和异常终止,格式
struct
{
# if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int __w_stopval:8; /* W_STOPPED if stopped. */
unsigned int __w_stopsig:8; /* Stopping signal. */
unsigned int:16;
# endif
} __wait_stopped; // 暂停,stop,格式
};
常用宏:
WIFEXITED(status),若为正常退出,则返回真。若为真,可用WEXITSTATUS(status)获取exit返回的状态;
WIFSIGNALED(status),若子程序为异常终止,则返回真(被信号终止),可用WTERMSIG(status)获取子进程终止的信号编号;可用WCOREDUMP(status)检查是否产生core文件,产生时为真;
WIFSTOPPED(status),如果子程序暂停,则为真,可通过WSTOPSIG(status),获取使子程序暂停的信号编号
WIFCONTINUED(status),若暂停后又继续的子进程返回状态,则为真,仅用于waitpid。
#define WIFEXITED(status) (((status) & 0x7f)== 0)
#define WIFSIGNALED(status) (((signed char) (((status) & 0x7f) + 1) >> 1) > 0)
#define WIFSTOPPED(status) (((status) & 0xff) == 0x7f)
#define WEXITSTATUS(status)(((status) & 0xff00) >> 8)
#define WTERMSIG(status) ((status) & 0x7f)
#define WSTOPSIG(status) (((status) & 0xff00) >> 8)
返回值:
成功:已结束运行(被等待的)的子进程的进程号
失败:-1 pid_t waitpid( pid_t pid, int *status, int options );
参数:
pid: >0,等待进程ID=pid的子进程,不管别的;
=-1,等待任何一个子进程,与wait()作用一样;
= 0,等待“组ID==调用进程组ID”的任一子进程;
<-1,等待“组ID==pid绝对值”的任一子进程
status:同wait()函数
options:sya
WNOHANG:不阻塞
WUNTRACED:若实现某支持作业控制,则由 pid 指定的任一子进程状态已暂停,且其状态自暂停以来还未报告过,则返回其状态
0:同wait(),阻塞
返回值:
正常:已结束运行的子进程的进程号
使用WNOHANG且没有子进程:0
调用出错:-1 【注意】
1. 关于几个测试退出状态的特殊的宏
/* 7-4,waitpid */
#include <stdio.h> // printf
#include <stdlib.h> // exit
#include <unistd.h>
#include <sys/types.h> // pid_t
#include <sys/wait.h>
int main(int args, char *argv[])
{
pid_t pid_fork,pid_wait;
int status;
pid_fork=fork();
if( pid_fork == 0 ) // child
{
sleep(5);
exit(0);
}
else if( pid_fork > 0 )
{
do
{
pid_wait = waitpid(pid_fork,&status,WNOHANG);
if( pid_wait != pid_fork )
printf("child thread %d is not over\r\n",pid_fork);
else
printf("child thread %d is over,status 0x%x\r\n",pid_fork,status);
sleep(1);
}
while(pid_wait!=pid_fork);
}
else
{
printf("fork err code %d.\r\n",pid_fork);
}
exit(0);
}
$ ./example
child thread 7575 is not over
child thread 7575 is not over
child thread 7575 is not over
child thread 7575 is not over
child thread 7575 is not over
child thread 7575 is over,status 0x0
$ ./example // 子进程exit(-1)时,waitpid的获取的值是0xff00,高位是exit的返回值,需要用到宏了
child thread 7605 is not over
child thread 7605 is not over
child thread 7605 is not over
child thread 7605 is not over
child thread 7605 is not over
child thread 7605 is over,status 0xff00
7.3 守护进程
7.3.1 守护进程概念
守护进程,也叫deamon进程,是后台服务进程;系统引导载入时启动,系统关闭时终止,独立于控制终端;常用于周期性的执行某种任务或等待处理某些事件。守护进程已d结尾,例如crond、lpd等。
控制终端:系统与用户进行交流的界面称为终端,每个从此终端开始运行的进程都会依赖这个终端,这个终端就是这些进程的控制终端。控制终端关闭时,相应的进程都会关闭。但是守护进程不受影响。
守护进程不受终端、用户和其他变化的影响,直到系统关闭时才退出。
7.3.2 编写守护进程
步骤:
7.3.2.1 创建子进程,父进程退出
父进程退出后,子进程编程了孤儿进程,被init进程收养。 形式上做到了与控制终端的脱离。
!!!7.3.2.2 在子进程中创建新会话
先了解基本概念:进程组、会话组、会话期
进程组:一个或多个进程的集合,每个进程组都有一个组长进程,进程组ID=组长PID
会话组:一个或多个进程组的集合
会话期:通常一个会话开始于用户登录,终止与用户退出,在此期间该用户运行的所有进程都属于这个会话期。
setsid():创建新的会话,并担任该会话组的组长。调用后起到3个作用:
- 让进程摆脱原会话的控制;
- 让进程摆脱原进程组的控制
- 让进程摆脱原控制终端的控制
总之,跟之前的控制终端、进程组、会话组都没有关系了。使进程完全独立出来,从而摆脱所有其他进程的控制 #include <sys/types.h>
#include <unistd.h> pid_t setsid( void );
返回值:
成功:该进程组ID
出错:-1
7.3.2.3 改变当前目录为根目录
通常的做法是将守护进程的当前目录设置为根目录,用chdir()系统调用。
7.3.2.4 重设文件权限掩码
umask(0),基本思路是给最大权限。
7.3.2.5 关闭文件描述符
父进程那继承来的文件描述符,一般不用,浪费,关闭。 连基本的输入输出都没用了,setsid时已经失去联系了,可以关了。
/* 7-5,deamon */ #include <stdio.h> // printf
#include <stdlib.h> // exit
#include <unistd.h>
#include <sys/types.h> // pid_t
#include <fcntl.h> int main(int args, char *argv[])
{
pid_t pid_fork;
int i;
int fd;
char buf[]="The deamon info.\n"; pid_fork = fork();
if( pid_fork < )
{
printf("fork err.\r\n");
}
else if( pid_fork > )
{
exit();
} // only child enter
setsid();
chdir("/");
umask();
for( i=;i<getdtablesize();i++ ) // 终端也关了,printf没有效果了,需要用别的调试方法
close(i); if( fd=open("/tmp/log", O_RDWR|O_CREAT,) < )
printf("open file err\r\n");
while()
{
write(fd,buf,sizeof(buf));
sleep();
} exit();
}
7.3.3 守护进程的出错处理
printf不好使,咋办?用linux提供的syslog服务,系统中有syslogd守护进程。不通版本linux的syslog日志文件的位置可能不通。
#include <syslog.h> void openlog( char * ident, int options, int facility );
参数:
ident:向每个消息加入的字符串,通常为程序的名称;
option:LOG_CONS,如果消息无法送到系统日志服务,则直接输出到系统控制终端
LOG_NDELAY:立即打开系统日志服务的连接。在正常情况下,直接发送到第一条消息时才打开连接
LOG_PERROR:将消息也同时送到 stderr 上
LOG_PID:在每条消息中包含进程的 PID facility: 指定程序发送的消息类型
LOG_AUTHPRIV:安全/授权信息
LOG_CRON:时间守护进程(cron 及 at)
LOG_DAEMON:其他系统守护进程
LOG_KERN:内核信息
LOG_LOCAL[0~7]:保留
LOG_LPR:行打印机子系统
LOG_MAIL:邮件子系统
LOG_NEWS:新闻子系统
LOG_SYSLOG:syslogd 内部所产生的信息函数传入值
LOG_USER:一般使用者等级信息
LOG_UUCP:UUCP 子系统 void syslog(int priority, char *format, ...)
参数: priority ,指定消息的重要性,
LOG_EMERG:系统无法使用
LOG_ALERT:需要立即采取措施
LOG_CRIT:有重要情况发生
LOG_ERR:有错误发生
LOG_WARNING:有警告发生
LOG_NOTICE:正常情况,但也是重要情况
LOG_INFO:信息消息
LOG_DEBUG:调试信息
format,同printf void closelog( void )