1、进程概念
进程是一个动态实体,是程序的一次执行过程,是操作系统资源分配的基本单位。
线程基本上不拥有系统资源,它与同属于一个进程的其他线程共享进程拥有的全部资源。
Linux下可以通过ps或pstree查看当前系统中的进程。
2、进程标识
PID:
进程控制符(PID),英文全称为Process Identifier。也常被称为进程标识符。顾名思义,它是各进程的身份标识,程序一运行系统就会自动分配给进程一个独一无二的PID。进程中止后PID被系统回收,可能会被继续分配给新运行的程序。进程的标识信息可通过函数获得,这些函数声明在unistd.h头文件中。列表如下:
(1)、pid_t getpid(id) 获得目前进程的进程ID
定义函数:pid_t getpid(void)
返回值:返回当前进程的进程识别号。
头文件:#include <unistd.h>
(2)、pid_t getppid(id) 获得父进程ID
功能:用来获取目前进程的父进程标识。
定义函数:pid_t getppid(void)
返回值:返回当前进程的父进程识别号。
头文件:#include <unistd.h>
(3)、pid_t getuid() 获得进程的实际用户ID(标识运行该进程的用户)
功能:获得进程的实际用户ID
定义函数:pid_t getuid(void)
返回值:返回进程的实际用户ID
实际用户ID(uid) :标识运行该进程的用户例如:一个普通用户A,运行了一个程序,而这个程序是以root 身份来运行的,则 程序运行时就具有root 权限。此时,实际用户ID时A用户的ID,而有效用户ID是root用户ID
头文件:#include <unistd.h>
(4)、 geteuid()
功能:获得进程的有效用户ID
定义函数:pid_t geteuid(void)
返回值:返回进程的有效用户ID
头文件:#include <unistd.h>
(5)、getpgid()
功能:用来获得参数pid指令进程所属于的组识别号,若参数为0,则返回当前进程的组识别码。
定义函数:pid_t getpgid(pid_t pid)
返回值:执行成功则返回正确的组识别码,若有错则返-1,错误原因存在于errno中。
头文件:#include <unistd.h>
(6)、getpgrp()
功能:用来获得目前进程所属于的组识别号,等价于getpgid(0)。
定义函数:pid_t getpgrp(void)
返回值:执行成功则返回正确的组识别码。
头文件:#include <unistd.h>
(7)、getpriotity(void)
功能:用来获得进程,进程组和用户的进程执行优先权。
定义函数:int getpriority(int which,int who)
参数含义:
which:
PRIO_PROCESS who为进程的识别码
PRIO_PGRP who为进程的组识别码
PRIO_USER who为用户识别码
返回值:执行成功则返回当前进程的优先级(-20--20),值越小优先级越高。若出错则返-1,原因在errno中。
头文件:#include <sys/resource.h>
3、Linux进程状态
在include/linux/sched.h 中我们可以看到Linxu中进程状态的具体实现:
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_ZOMBIE 4
#define TASK_STOPPED 8
其中:
TASK_RUNNING,运行状态:进程正在运行或在运行队列中等待运行 。
TASK_INTERRUPTIBLE,可中断等待状态:进程正在等待某个事件完成(如等待数据到达)。等待过程中可以被信号或定时器唤醒。
TASK_UNINTERRUPTIBLE,不可中断等待状态:进程正在等待某个事件完成并且等待中不可以被信号或定时器唤醒,必须一直等待到事件发生。
TASK_ZOMBIE,僵死状态:进程已终止,但进程控制块尚未注销,进程描述符依然存在,直到父进程调用wait()函数后释放。
TASK_STOPPED,挂起/停止状态:进程因为收到SINSTOP,SIGSTP,SIGTIN,SGIOU信号后停止运行或者该进程正在被跟踪。
注:ps命令可以查看进程的当前状态:运行
[......]$ ps -eo pid,stat
PID STAT
1 S
2 S< (后缀字符解释:< 高优先级进程)
3 SN (后缀字符解释:N 低优先级进程)
4 SL (后缀字符解释:L 内存锁页)
5 Ss (后缀字符解释:s 该进程为会话首进程)
6 Sl (后缀字符解释:l 多线程进程)
7 S+ (后缀字符解释:+ 进程位于前台进程组)
4、进程控制
fork 创建一个新进程
exit 终止进程
exec 执行一个应用程序
wait 将父进程挂起,等待子进程终止
getpid 获取当前进程ID
nice 用于改变进程的优先级
5、程序转化为进程的步骤:
(1)、内核将程序读入内存,为程序分配内存空间
(2)、内核为该进程分配进程标识符(PID)和其他所需资源
(3)、内核为该进程保持PID及相应的状态信息,把进程放到运行队列中等待执行。程序转化为进程后就可以被操作系统的调度程序调度执行了。
二者区别:
可执行程序位于磁盘中、没有堆栈、静态的、不变的。
内存映像位于内存中、动态变化。
//====进程相关函数==============(转xuejianhui)一个具体的进程程序======================================= #include <unistd.h> //成功返回进程ID,否则返回-1。 pid_t getpid(void); //获取进程ID pid_t getppid(void);//获取父进程ID pid_t getuid(void); //获取进程用户ID pid_t geteuid(void);//获取进程有效用户ID pid_t getgid(void); //获取进程组ID pid_t getegid(void);//获取进程有效组ID pid_t fork(void); //对父进程,返回新创子进程ID;对子进程,返回0;失败返回-1。 //子进程复制父进程的堆栈段和数据段的内容,但和父进程共用代码段。 // 父子进程的不同点: // 1. 进程ID和父进程ID不同,但调度机会均等。 // 2. 子进程的time_utime,time_stime,time_cutime,time_ustime被清零。 // 3. fork函数返回值不同。 // 4. 文件锁不会被继承。 // 5. 子进程清理未处理的闹钟信号和未决信号。 // 导致fork失败的原因: // 1.系统已有太多进程; // 2.调用fork函数的用户的进程太多; pid_t vfork(void); //创建公用父进程地址空间的子进程 // vfork 与 fork 的区别: // vfork 产生的子进程与父进程公用地址空间,可影响父进程。更像是线程。 // vofrk 产生的子进程一定先运行,父进程等待子进程运行完以后再运行。 // 注意:不要在任何函数中调用vfork; int setuid(uid_t uid); //改变进程的实际用户ID和有效用户ID,成功返回0;失败返回-1. #include <stdlib.h> void exit(int status); //退出线程 // 可使用"echo $?"命令来查看程序退出值 // return 1; == exit(1); #include <sys/wait.h> pid_t wait(int *statloc);//获取子进程结束状态 // 返回子进程ID,并将结束信息保存在statloc指向的内存空间 // 状态: 判断宏 : 取值宏: // 进程正常退出 : WIFEXITED(status) WEXITSTATUS(status) // 进程异常退出 : WIFSIGNALED(status) WTERMSIG(status) // 进程暂停 : WIFSTOPPED(status) WSTOPSIG(status) // 只能等待第一个退出的进程 pid_t waitpid(pid_t pid, int *statloc, int options);//用于等待指定的进程退出 //参数pid : -1,任意子进程; >0,进程ID == pid; 0, 组ID == pid; < -1, 组ID == pid绝对值; //参数options : // WCONTINUED : 当子进程在暂停后继续运行,且其状态未上报,则返回其状态; // WNOHANG : 当所等待的进程未结束运行时阻塞,waitpid直接返回; // WUNTRACED : 子进程暂停时,其状态一直未上报,则返回其状态; #include <signal.h> void (*signal(int signo, void (*func) (int))) (int); //参数signo : 需要加载处理的信号编号,例如SIGKILL等.编号是整数宏,定义在signal.h中。 //参数func : 函数指针,捕捉信号后的响应函数,该参数有以下3个可能值 : // SIG_IGN : 忽略该信号,该宏在signal.h中定义: #define SIG_IGN ((void *)(*)()) 1 // SIG_DFL : 默认处理方式,该宏在signal.h中定义: #define SIG_IGN ((void *)(*)()) 0 // 其他已定义的指针函数 : 信号处理函数原型: void handler(int); // 函数的返回值是一个函数指针,该函数指向上一次的信号处理程序。该函数与参数func表示的一致 // 如果出错,则signal返回SIG_ERR(在signal.h中定义,#define SIG_ERR ((void *)(*)()) -1)。 //由于signal函数过于复杂,使用typedef进行如下简化: typedef void HANDLER (int); HANDLER *signal (int signo, HANDLER *handler); //Linux不允许用户创建新信号,但提供SIGUSR1和SIGUSR2专门用于应用程序间进行信号通信。 int kill(pid_t pid, int signo);//向另外一个进程发送信号 //参数pid的取值及语义: // >0,发送给进程ID为pid的进程; // =0,发送给进程组ID和该进程相同的进程; // <0,发送给进程组内进程ID为pid的进程; //=-1,发送给系统所有的进程; //--------------------------------------------------------------------------------------6、进程操作
(1)、创建进程
a、进程创建两种方式:操作系统创建、父进程创建。
b、子进程创建以后,父进程和子进程争夺CPU,抢到CPU者执行,另一个挂起等待。若需要父进程等待子进程执行完毕以后再继续执行,可以在fork操作后调用wait或waitpid函数。
c、fork函数一次调用有两个返回值,一是父进程调用fork函数后的返回值,该返回值是刚刚创建的子进程ID,另一个是子进程中fork函数的返回值0,创建失败只返回-1。
d、fork之后父进程和子进程哪一个先执行是不确定的,取决于调度算法。
e、如果一个子进程的父进程先于子进程结束,子进程就成为一个孤儿进程,它由init进程收养,成为init进程的子进程。
f、vfork()与fork()的区别
fork创建一个子进程是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。系统开销大。
vfork创建的子进程共享父进程的的地址空间,也就是完全运行在父进程的地址空间上。子进程修改数据,父进程也就修改了。vfork保证子进程先运行,当调用exit或exec之后,父进程才可能被调度运行。系统开销小。(此种情况下,不要容许子进程修改与父进程共享的全局变量和局部变量)
注:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
char* msg;
int k;
printf("....\n");
pid = fork();
switch(pid){
case 0:
msg = "hhhh";
k = 3;
break;
case -1:
perror("...failed");
break;
default:
msg = "ppp";
k = 5;
break;
}
while(k > 0)
{
puts(msg);
sleep(1);
k--;
}
exit(0);
}
(2)、创建守护进程
守护进程是指在后台运行的、没有控制终端与之相连的进程,通常周期性的执行某种任务。
守护进程的启动方式:a、启动脚本/etc/rc.d中启动。b、作业规划进程crond启动。c、终端执行(shell)。
编写守护进程要点:
a、让进程后台执行。fork产生一个子进程然后让父进程退出。
b、调用setsid是进程成为一个新的会话组长和进程组长。
c、禁止进程重新打开控制终端。再一次由fork创建新的子进程,使调用fork的进程退出。
d、关闭不再需要的文件描述符,否则浪费资源。
e、将当前目录更改为根目录。
f、将文件创建时使用的屏蔽字设置为0.用umask(0)将屏蔽字清零。
g、处理SIGCHLD信号。
创建守护进程方法举例:
#incldue <stdio.h>
...
int init_daemon(void)
{
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信号*/
return 0;
}
注:chdir 是C语言中的一个系统调用函数(同cd)
功 能:更改当前工作目录。
参 数:Path 必选。Path 可能包含驱动器。如果未指定驱动器,则当前驱动器上的默认目录或文件夹。
返回值:成功返回0 ,失败返回-1
注:进程退出
正常退出 return exit _exit
异常退出 调用abort 进程收到某个信号,而该信号使程序终止
exit在头文件stdlib.h中,调用exit()要先执行一些清除操作,然后将控制权交给操作系统。_exit()声明在unistd.h中,执行后会立即返回给操作系统。
7、执行新程序
使用fork或vfork创建子进程后,我们希望子进程去执行其他程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。对系统而言,不过执行的是另外一个程序了。
a、exec函数族如下:
int exec…装入和运行其它程序:
int execl( char *pathname,char *arg0,char *arg1,...,char *argn,NULL)
int execle( char *pathname,char *arg0,char *arg1,...,char *argn,NULL,char *envp[])
int execlp( char *pathname,char *arg0,char *arg1,...,NULL)
int execlpe(char *pathname,char *arg0,char *arg1,...,NULL,char *envp[])
int execv( char *pathname,char *argv[])
int execve( char *pathname,char *argv[],char *envp[])
int execvp( char *pathname,char *argv[])
int execvpe(char *pathname,char *argv[],char *envp[])
事实上,这6个函数中真正的系统调用只有execve,其他5个都是库函数,它们最终都会调用execve这个系统调用,调用关系如下图所示:
b、环境变量:
为了更好的理解exec函数族的使用,首先要理解环境变量这个概念:环境变量包括用户主目录、终端类型、当前目录等,他们定义了用户的工作环境。env命令可以查看环境变量值或修改这些变量值。在每个进程启动时,都会接到一张环境表。环境表是一个字符指针数组,其中每个指针包含一个以 null 结束的 C 字符串的地址。全局变量environ 则包含了该指针数组的地址,
extern char **environ;
例如,下图显示了包含有5个环境字符串的环境表
c、环境变量值获取函数(转)
ISO C 定义了一个函数 getenv,用于获取环境变量值。
#include <stdlib.h>
char *getenv(const char *name);
返回值:指向与name关联的value的指针,若为找到则返回NULL
#include <stdlib.h>
int putenv(char *str);
返回值:若成功则返回0,若出错则返回非0值
#include <stdlib.h>
int setenv(const char *name, const char *value, int rewrite);
返回值:若成功则返回0,若出错则返回-1
setenv 函数将环境变量 name 的值设置为 value。如果环境表中 name 已存在,那么• 若 rewrite 为非 0 值,则首先删除其现有定义。
• 若 rewrite 为 0,则不删除其现有定义,name 不设置为新的 value,也不出错。
#include <stdlib.h>
int unsetenv(const char *name);
返回值:若成功则返回0,若出错则返回-1
unsetenv 函数删除 name 的定义,即使不存在环境变量 name 也不出错。
int main(int argc, char *argv[], char **environ)//这是main函数最完整的形式
{
pid_t pid;
int stat_val;
pid = fork();
switch(pid){
case -1:
perror("..error.");
exit(1);//异常退出,控制权归还给操作系统
case 0:
printf("uid=%d,gid=%d", getuid(),getgid());
execve("processimage", argv, environ);//该函数,使得进程执行新程序
printf("process never go to here\n")://此处子进程执行不到这里
exit(0);//正常退出,控制权归还给操作系统
default:
...
break;
}
wait(&stat_val);//如果父进程没有调用wait和waitpid函数,子进程会进入僵死状态,调用了就不会了。
exit(0);
}
注:wait函数使父进程暂停执行,直到它的一个子进程结束为止,该函数的返回值是终止运行的子进程PID。wait等待第一个终止的子进程,而waitpid则可以指定等待特定的子进程。
8、进程其他操作
(1)、获得进程ID
getpid();
举例:printf("get ID %d",getpid());
(2)、设置实际用户ID和有效用户ID
int setuid(uid_t uid)
使用方法:若进程具有root权限,则函数将实际用户ID、有效用户ID设置为参数uid。若进程不具有root权限,但uid等于实际用户ID,则setuid只将有效用户ID设置为uid,不改变实际用户ID。其他情况函数调用失败,返回-1并将erron设置为EPERM。setuid()用来重新设置执行目前进程的用户识别码。不过,要让此函数有作用,其有效的用户识别码必须为0(root)。
(3)、设置实际组ID和有效组ID
int setgid(gid_t gid)
(4)、改变进程优先级
在Linux下通过系统调用nice可以改变进程的优先级:
a、int getpriority(int which, int who)
该函数返回一组进程的优先级,which和who组合确定返回哪一组进程的优先级。
参数:
PRIO_PROCESS:一个特定的进程,此时who取值为进程ID
PRIO_PGRP:一个进程组的所有进程,who为进程组的ID
PRIO_UESR:一个用户拥有的所有进程,who取值为实际用户ID
b、int setpriority(int which, int who, int prio)
该函数用来设置指定的进程优先级,函数返回指定进程的优先级。
nice用法代码举例:
int main(void)
{
pid_t pid;
int stat_val = 0;
int oldpri,newpri;
pid = fork();
switch(pid){
case 0:
oldpri = getpriority(PRIO_PROCESS, 0);
newpri = nice(2);//此处改变了进程的优先级,设置为2
...
}
}