Linux进程
进程是Linux中事务管理的基本单元,所有的进程都拥有自己的独立处理环境和系统资源,并且各进程之间不可以直接访问对方的资源,进程之间的交流需要通过特定的机制(IPC)。
在Linux系统的内核头文件中(/usr/src/kernels/内核版本/include/linux/sched.h)定义了进程控制块(PCB)结构体struct task_struct来管理每个进程的资源。
进程资源分为两个部分:内核空间进程资源、用户空间进程资源
内核空间进程资源:PCB的相关信息,控制块本身、打开的文件表项、当前目录、当前终端信息、线程基本信息、可访问的内存空间、PID、PPID、UID、EID等。
用户进程空间资源:通过成员mm_struct映射的内存空间,实际上就是进程的代码段、数据段、堆、栈、可访问的共享库内存空间。这些资源在进程退出的时候主动释放。在进程运行时,可以通过查
看/proc/{pid}/maps文件查看可以访问的地址空间。
进程的状态:就绪、运行、等待(可中断、不可中断)、停止、僵死
在/usr/src/kernels/内核版本/include/linux/sched.h头文件中定义了进程的状态
/* * Task state bitmask. NOTE! These bits are also * encoded in fs/proc/array.c: get_task_state(). * * We have two separate sets of flags: task->state * is about runnability, while task->exit_state are * about the task exiting. Confusing, but this way * modifying one set can't modify the other one by * mistake. */ #define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define __TASK_STOPPED 4 #define __TASK_TRACED 8 /* in tsk->exit_state */ #define EXIT_ZOMBIE 16 #define EXIT_DEAD 32 /* in tsk->state again */ #define TASK_DEAD 64 #define TASK_WAKEKILL 128 #define TASK_WAKING 256 #define TASK_PARKED 512 #define TASK_STATE_MAX 1024进程之间状态的转换图如下:
进程的属性
进程本身的属性:PID(进程号)、PPID(父进程号)、PGID(进程组号)、SID(会话session ID)
调用getpid()函数可以获取当前进程的PID,该函数在/usr/include/unistd.h头文件中声明;
调用getppid()函数可以获取当前进程父进程的PID,该函数在/usr/include/unistd.h头文件中声明;
调用getpgid(_pid_t _pid)函数可以获取指定进程_Pid的进程组号,该函数在/usr/include/unistd.h头文件中声明;
调用getsid(_pid_t _pid)函数可以获取指定进程_Pid的会话号SID(会话是一个或者多个进程的集合),该函数在/usr/include/unistd.h头文件中声明;
进程的用户属性:RUID(真实用户ID)、RGID(真是用户组ID)、EUID(有效用户ID)、EGID(有效用户组ID)
调用getuid()函数可以获取该进程的真实用户RUID(执行此程序的用户,创建该进程用户的UID为此进程的RUID),该函数在/usr/include/unistd.h头文件中声明;
调用geteuid()函数可以获取有效用户ID,该函数在/usr/include/unistd.h头文件中声明;
调用getgid()获取进程的用户组ID(创建进程的用户所在的组号),该函数在/usr/include/unistd.h头文件中声明;
调用getegid()获取有效进程用户组ID,该函数在/usr/include/unistd.h头文件中声明;
进程的管理
创建进程fork 和 vfork
在Linux环境下,创建进程的主要方法是调用fork函数。Linux下的所有进程都是由init进程(第一个进程PID=1)直接或者间接的创建,fork函数在/usr/include/unistd.h头文件中声明;
若fork函数调用,那么将产生两个进程分支(父进程和子进程),在父进程中返回子进程的ID(大于0),在子进程中返回0;若调用失败,在父进程中返回0;
下面是创建调用fork函数的程序
#include<stdio.h> #include<unistd.h> int main() { pid_t pid; if( (pid=fork()) == -1 ) //call fork() and test if succeed { printf("fork error!"); return -1; } else if(pid == 0) //in sub thread { printf("This is sub thread.\n"); printf("in sub : My PID = %d\n",getpid()); printf("in sub : My PPID = %d\n",getppid()); } else //in father thread { printf("This is father thread.\n"); printf("in father : My PID = %d\n",getpid()); } return 0; }子进程与父进程
子进程将复制父进程用户空间的所有信息(文件缓冲区、代码段、数据段、BSS段、堆、栈、文件描述符),其中复制的仅仅是文件描述符,但是对于与文件描述符关联的内核文件表项(struct file结构体)则是采用的是共享的方式。
下面的程序中,虽然子进程复制了父进程的文件描述符fd,但是和父进程操作的是同一个文件
#include <sys/types.h> #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <stdlib.h> int main(int argc,char *argv[]) { pid_t pid; int fd; int i=1; int status; char *ch1="hello"; char *ch2="world"; char *ch3="IN"; if((fd=open("test.txt",O_RDWR|O_CREAT,0644))==-1)//打开一个文件 { perror("parent open"); exit(EXIT_FAILURE); } if(write(fd,ch1,strlen(ch1))==-1)//父进程向文件中写入字符串1 { perror("parent write"); exit(EXIT_FAILURE); } if((pid=fork())==-1)//创建一个子进程 { perror("fork"); exit(EXIT_FAILURE); } else if(pid==0) //在子进程中 { i=2; printf("in child\n"); printf("i=%d\n",i); if(write(fd,ch2,strlen(ch2))==-1)//子进程复制了父进程的fd,然后向文件中写入字符串2 perror("child write"); return 0; } else { sleep(1);//休眠1秒钟,等待子进程执行完毕 printf("in parent\n"); printf("i=%d\n",i); if(write(fd,ch3,strlen(ch3))==-1)//父进程写入字符串3 perror("parent,write"); wait(&status);//等待子进程结束 return 0; } }使用vfork创建进程
使用fork创建进程会复制父进程用户空间的大量数据,有些场景这样做是极大的浪费,所以使用vfork创建新进程时并不复制父进程的地址空间,而是在必要的时候才申请新的存储空间,vfork比fork可以极大程度的提高性能。vfork只在需要的时候复制,一般采用和父进程共享所有资源的方式处理。
下面的程序说明了vfork创建的子进程和父进程共享数据段和代码段。
#include<unistd.h> #include<error.h> #include<sys/types.h> #include<stdio.h> #include<stdlib.h> int glob=6; int main() { int var; pid_t pid; var=88; printf("in beginning:\tglob=%d\tvar=%d\n",glob,var); if((pid=vfork())<0) { perror("vfork"); exit(EXIT_FAILURE); } else if(pid==0) { printf("in child,modify the var:glob++,var++\n"); glob++; var++; printf("in child:\tglob=%d\tvar=%d\n",glob,var); _exit(0); } else { printf("in parent:\tglob=%d\tvar=%d\n",glob,var); return 0; } }上述程序共享glob和var变量,如果把上述中的vfork改为fork结果将会不一样。
结束进程
结束进程意味着要回收进程的资源,又可以分为回收进程用户空间的资源和回收进程内核空间的资源
回收用户进程空间的资源
进程在正常退出前都需要执行退出处理函数,刷新流缓冲区等操作,然后释放用户进程空间的所有资源,而进程在内核中的资源PCB并不会立即释放,仅仅调用退出函数而没有回收内核资源PCB的进程是一个僵死进程。
显示的调用exit或者_exit系统调用可以结束进程。调用exit将以反序的方式执行由on_exit函数和atexit函数注册的清理函数,同时刷新缓冲区,并把退出状态status返回给父进程。_exti是直接退出,仅仅把退出状态交给父进程,不会进行清理工作和刷新缓冲区。
注意:exit和return的区别,exit退出当前进程,而return仅仅退出当前函数。
回收内核空间的资源
回收内核空间的资源不是由退出进程本身完成的,而是由该进程的父亲进程完成的。调用wait函数的父进程将阻塞式的等待该进程的任意一个子进程结束后,回收该子进程的内核空间的资源,wait函数定义在/usr/include/sys/wait.h头文件中。还可以使用waitpid函数等待指定子进程结束,其定义在/usr/include/sys/Wait.h中
孤儿进程和僵死进程
孤儿进程:因为父进程退出而导致一个子进程被init进程收养的进程称为孤儿进程,即孤儿进程的父进程改为init进程。
僵死进程:进程已经退出(用户空间的资源已经被回收),但是其父亲进程还没有回收内核资源的进程称为僵死进程,即该进程在内核空间的PCB没有被释放。
下面的程序演示了一个孤儿进程
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main() { pid_t pid; if((pid=fork())==-1) perror("fork"); else if(pid==0) { printf("sub : pid=%d,ppid=%d\n",getpid(),getppid()); sleep(2); printf("sub : pid=%d,ppid=%d\n",getpid(),getppid()); } else { printf("father : pid=%d,ppid=%d\n",getpid(),getppid()); sleep(1); exit(0); } }下面的程序演示了一个僵死进程
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { pid_t pid; if((pid=fork())==-1) perror("fork"); else if(pid==0) { printf("child_pid pid=%d\n",getpid()); exit(0); } sleep(3); system("ps"); exit(0); }程序的运行结果:
运行结果中的PID=10375的进程僵死,如果此时打开/proc/10375/maps(该进程的用户进程空间信息),可以发现为空,也就是用户进程空间资源已经被回收。
守护进程
守护进程是一种在后台运行的特殊进程,它脱离终端,从而可以避免守护进程被任何终端信号打断,它执行产生的信息也不在任何终端显示。守护进程周期性的执行某种任务或等待处理某些发生的事件,Linux的大多数服务器就是使用守护进程实现的,比如web服务器httpd等。
一般情况下,守护进程可以通过以下方式启动:
(1)在系统启动时启动脚本,通常放在/etc/rc.d目录中;
(2)利用inet超级服务器启动,如telnet等;
(3)由cron命令定时启动以及在终端用nohup命令启动的进程也是守护进程。
创建一个守护进程需要遵循以下要点:
(1)屏蔽一个有关控制终端操作的信号
(2)在后台运行。方法是在进程中创建子进程,并使父进程终止,使其在子进程中后台运行。
(3)脱离终端控制和进程组,这是因为一个进程属于一个进程组,同进程组的进程共享一个控制终端,而一个进程关联的控制终端和进程组通常是从父进程继承下来的,因此子进程仍然受到父进程终端的影响。可以使用setsid()使子进程成为一个新的会话组长和进程组长,与原来的进程组脱离关系。
(4)禁止进程重新打开控制终端。进程已经成为一个无终端的会话组长,但是仍然有权限申请一个终端,只有会话组长才能打开终端。采用的办法是再创建一个子进程,并让父进程退出,该子进程就不再是会话组长了,而且父进程也没有终端控制。
(5)关闭打开的文件描述符。子进程会从父进程复制文件描述符,这对守护进程没有意义,反而会浪费系统资源,造成进程所占的文件系统无法卸载。
(6)改变当前工作目录。进程活动的时候,进程工作目录所在的文件系统不能卸载,因此一般将守护进程的工作目录设置为合适的目录。
(7)重设文件创建的掩码。进程从父进程那里继承文件创建掩码umask,它可能修改守护进程所创建的文件的存储权限,一般将文件创建掩码清除。
(8)处理SIGCHLD信号(子进程退出信号)