一、进程
- 进程是一个动态实体,是程序的一次执行过程,是操作系统资源分配的基本单位。进程是运行中的程序,程序是一些保存在硬盘上的可执行代码。
二、进程结构
Linux中进程由三部分组成:代码段,数据段,堆栈段
代码段(存放可执行代码) | 数据段(存放程序全局变量,常量,静态变量) | 堆栈段(堆存放动态分配的内存变量,栈用于函数调用,存放函数的参数,函数内部定义的局部变量) |
三、创建进程
-
进程创建的两种方式
- 由操作系统创建:
由操作系统创建的进程之间的关系平等,一般不存在资源继承关系。
- 由父进程(创建该进程的进程称父进程)创建:
对于父进程创建的进程(子进程),子进程和父进程存在隶属关系。父子进程共享代码段,子进程还获得父进程数据段、堆、栈的复制。
-
fork函数
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void)
调用fork函数后,进程实际上已经分裂为父子两个进程,父子进程在调用fork函数处分开,fork函数有两个返回值,一个是父进程调用fork后的返回值,返回创建的子进程的ID;另一个是子进程中fork函数的返回值,0。返回两次的前提是进程创建成功,若失败则只返回-1。
-
下面是一个父子进程交替执行的例子:
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
char* msg;
int k;
printf("Processs Creation Study\n");
pid = fork();
switch(pid) {
case 0:
msg = "Child process is running";
k = 3;
break;
case -1:
perror("Process creation failed\n");
break;
default:
msg = "Parent process is running";
k = 5;
break;
}
while(k > 0){
puts(msg);
sleep(1);
k--;
}
exit(0);
}
运行结果:
Processs Creation Study
Parent process is running
Child process is running
Parent process is running
Child process is running
Parent process is running
Child process is running
Parent process is running
Parent process is running
- 如果一个子进程的父进程先结束,子进程就被称为孤儿进程,由init进程收养,成为init进程的子进程
下面是一个孤儿进程:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
switch(pid) {
case 0:
while(1) {
printf("A background proccess,PID:%d\n, ParentID: %d\n",getpid(),getppid());
sleep(3);
}
case -1:
perror("Proccess creation failed\n");
exit(-1);
default:
printf("I am parent proccess,my pid is %d\n",getpid());
exit(0);
}
return 0;
}
以上是fork成功创建进程的过程,在失败时返回-1,失败的原因通常是父进程拥有的子进程的个数超过了规定的限制,此时errno值为EAGAIN,内存不足也会导致进程创建失败,此时errno值为ENOMEM。
子进程会继承父进程的用户ID,组ID,当前工作目录,根目录,打开的文件,创建文件时用的屏蔽字,上下文环境,共享的存储段,资源限制等。也有一些与父进程不一样的属性:
- 子进程有自己唯一的进程ID
- fork返回值不同
- 不同的父进程ID。子进程的父进程ID为创建他的父进程ID
- 子进程共享父进程打开的文件描述符,但父进程对文件描述符的改变不会影响子进程中的文件描述符
- 子进程不继承父进程设置的文件锁
- 子进程不继承父进程设置的警告
- 子进程的未决信号集被清空
-
vfork函数
- vfork和fork一样都是调用一次,返回两次
- fork创建的子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,有良好的并发性。而使用vfork创建一个子进程时,操作系统并不将父进程的地址空间完全复制到子进程,用vfork创建的子进程共享父进程的地址空间,也就是说子进程完全运行在父进程的地址空间上,子进程对该地址空间中任何数据的修改同样为父进程所见。
- fork创建进程,父子那个先运行取决于系统的调度算法。而vfork保证子进程先运行,当他调用了exec或exit后,父进程才可能被调度。如果在调用exec或exit之前子进程要依靠父进程的某个行为,就会导致死锁。
下面这个例子对比fork和vfork的区别:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int globVar = 5;
int main()
{
pid_t pid;
int var = 1, i;
printf("fork is differentwith 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);//break;
case -1:
perror("Process creation failed\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运行的结果:
fork is differentwith vfork
Parent process is running
Child process is running
Parent process is running
Child process is running
Parent process is running
Child process is running
Parent process is running
Child's globVar = 8,var = 4
Parent process is running
Parent's globVar = 10 ,var = 6
子进程继承了父进程的全局变量和局部变量,由最后父子进程globVar和var的结果可以证明fork的子进程有自己独立的地址空间。
再用vfork执行一次:
fork is differentwith vfork
Child process is running
Child process is running
Child process is running
Child's globVar = 8,var = 4
Parent process is running
Parent process is running
Parent process is running
Parent process is running
Parent process is running
Parent's globVar = 13 ,var = 9
同理可以推出vfork的子进程共享父进程的地址空间。
-
守护进程
-
什么是守护进程?
守护进程是指在后台运行的,没有控制终端与之相连的进程。他独立于控制终端,通常周期性的执行某种任务。
-
守护进程有什么用?
Linux的大多数服务器就是用守护进程的方式实现的,如Internet服务器进程inetd,Web服务器进程http等。守护进程在后台运行,类似于Windows中的系统服务.
-
守护进程的启动方式
守护进程可以在Linux系统启动时从启动脚本/etc/rc.d中启动;可以由作业规划进程crond启动;还可以由用户终端(Shell)执行。
-
编写创建守护进程的要点
- 让进程在后台运行。调用fork产生一个子进程,然后使父进程退出
- 调用setsid创建一个新对话期。控制终端,登陆会话和进程组通常是从父进程继承下来的,守护进程要摆脱他们,不受他们影响,方法是调用setsid使进程成为一个会话组长
- 禁止进程重新打开终端。再次通过fork创建新的子进程,使调用fork的进程退出
- 关闭不需要的文件描述符。子进程从父进程继承打开的文件描述符。为了不浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。先得到最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符
- 将当前目录更改为根目录。当守护进程当前工作目录在一个装配文件系统中时,该文件系统不能被卸载,一般需要将工作目录改为根目录
- 将文件创建时使用的屏蔽字设置为0。进程从创建他的父进程那里继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用umask(0)将屏蔽字清0
- 处理SIGCHLD信号
下面是创建守护进程的代码:
#include <stdio.h>
#include <sys/param.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <time.h>
#include <syslog.h>
int init_daemon()
{
int pid;
int i;
//忽略终端I/O信号,STOP信号
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP,SIG_IGN);
pid = fork();
if(pid > 0) {
exit(0); //结束父进程,使子进程成为后台进程
}
else if(pid < 0){
return -1;
}
//建立一个新的进程组,在这个新的进程组中,子进程成为这个进程组的首进程,以使该进程脱离所有终端
setsid();
//再次新建一个进程,退出父进程,保证该进程不是进程组长,同时让该进程无法再打开一个新的终端
pid = fork();
if(pid > 0){
exit(0);
}
else if(pid < 0){
return -1;
}
//关闭所有从父进程继承的不再需要的文件描述符
for(i = 0; i <NOFILE; close(i++));
//改变工作目录,使进程不与任何文件系统联系
chdir("/");
//将文件屏蔽字设置为0
umask(0);
//忽略SIGCHLD信号
signal(SIGCHLD,SIG_IGN);
return 0;
}
int main()
{
time_t now;
init_daemon();
syslog(LOG_USER|LOG_INFO,"测试守护进程!\n");
while(1){
sleep(8);
time(&now);
syslog(LOG_USER|LOG_INFO,"系统时间:\t%s\t\t\n",ctime(&now));
}
}
编译运行,然后用ps -ef查看进程状态:
UID PID PPID C STIME TTY TIME CMD
crdbyb 24682 2329 0 15:26 ? 00:00:00 ./a.out
四、进程退出
-
两种退出方式
-
正常退出
- 在main函数中执行return
- 调用exit函数
- 调用_exit函数
-
异常退出
- 调用about函数
- 进程收到某个信号,而该信号使程序终止
两种退出方式都会执行内核中的同一段代码。这段代码用来关闭进程所有已打开的文件描述符,释放他所占用的内存和其他资源
-
退出方式的比较
- exit和return,exit是一个函数,有参数,而return是函数执行完后的返回。exit把控制权交给系统,而return将控制权交给调用函数
- exit和about,exit是正常终止进程,而about是异常终止
- exit(int exit_code):exit中的参数exit-code为0代表进程正常终止,若为其他值表示程序执行过程中有错误发生
- exit()和_exit()的区别:exit在头文件stdlib.h中声明,而_exit()声明在头文件unistd.h中。两个函数均能正常终止进程,但是_exit()会执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核