进程的概念
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
依照我简单地理解:进程就是正在运行中的应用程序,它是一个动态的概念,他可以申请和拥有系统资源 。我们用c语言写的程序执行起来会产生进程。它具有动态性,并发行,独立性,异步行,交互性的特点。
特点解释
动态性不用解释,上面根据概念可以理解,并发性可以理解为一台计算机可以同时运行多个程序,比如你用一台计算机在登qq的同时可以打开视频播放器看电影,看起来这两件事是在一台计算机上是同时发生的,其实并不是,一个cpu不可能同时处理多个进程资源请求,只不过是cpu运行速度我们是无法用肉眼判别的,他每次只处理一个资源请求,当有多个进程请求资源,会出现多个进程抢cpu的情况,cpu会根据【调度算法】决定给那个程序分配资源,具体的调度算法实现,可以度娘,比较经典的很多,所以说进程的并发不是实际的并发,而是我们宏观上理解的并发,cpu终究是不能同一时刻处理多个进程的。独立性,可以理解为进程实体是一个可以独立运行的单位,同时也是系统中独立获得资源和调度的基本单位。异步性,由于计算机运行时多个进程之间相互制约(可以理解为抢cpu),导致进程出现“执行–暂停–执行….”,这种“走走停停”的以不可预知的速度运行的局面,而异步性就是这种不可预知的速度,何时开始何时暂停何时结束的性质。最后一个就是进程之间的交互性,也可以说是进程之间的通信,进程之间的通信有多种方式可以度娘一下,有详解,这一次进程学习并没有研究这方面的内容。
进程的标识
书上说是每个进程是通过唯一的id(非负数)标识的,也就相当于人的身份证号,这里要列出几种比较常用的获取进程id的函数
pid_t getpid()//获取进程id
pid_t getppid()//获取父进程id
pid_t getuid()//获取进程的实际用户id
pid_t geteuid()//获取进程的有效用户id
pid_t getgid() //获得进程的实际组id
pid_t getegid() //获得进程的有效组id
- 按照书上解释一下这些有效和实际的意义吧!
实际用户ID:标识该进程的用户ID
实际组ID:实际用户所属的组ID
有效用户:标识以什么身份来运行进程,例如:某个普通用户A,运行了一个程序,而这个程序是以root身份来运行的,这程序运行时就具有root权限,此时实际用户ID是A用户的ID,而有效用户ID是root用户的ID
有效组ID:有效ID是有效用户所属的组的组ID
进程的状态
就绪态。指进程已经获得所有所需的其他资源,正在申请处理处理器资源,准备开始执行。这种情况下,称进程处于就绪态。
阻塞态。指进程因为需要等待所需资源而放弃处理器,或者进程本不拥有处理器,且其他资源也没有满足,从而即使得到处理器也不能开始运行。这种情况下,进程处于阻塞态。阻塞状态也称休眠状态或者等待状态。
运行态。进程得到了处理器,并不需要等待其他任何资源,正在执行的状态,称之为运行态。只有在运行态时,进程才可以使用所申请到的资源。
僵死状态
进程已终止,但进程描述符依然存在,知道父进程调用wait后函数释放。
进程控制及内存映像
在linux系统中,进程的控制主要系统调用如下:
- fork:同于创建一个新进程。
函数原型:
#include<sys/types.h>
#include<unistd.h>
pid_t fork();
这个函数返回值有两个,一个是创建的子进程的ID,另一个是0或-1,我对其理解是:父进程在调用fork函数后,若创建成功,一个运行的程序会变成两个,即父进程和子进程,父进程返回创建的子进程的ID号,子进程中fork函数会再次返回一个0,所以fork()函数要想有两个返回值,子进程创建成功是前提。
所以一般创建子进程判断是否成功方法如下(省略主函数和头文件):
pid_t pid;//
if((pid=fork())<0){
printf("创建进程失败!\n");
return ;
}
else if(pid == 0){
printf("子进程正在跑!\n");
}
else{
printf("父进程正在跑!\n");
}
一般用fork创建子进程,不能确定创建成功之后是子进程先跑还是父进程先跑,cpu对两个进程的对待是平等的,只能看他们谁能抢到cpu,那谁就先执行。就有了基于fork()函数上的vfork()函数,用vfork创建子进程后,一定是子进程先跑,当他调用exec或exit之后,父进程才有可能被调度运行,如果在调用这两个函数之前,子进程要依赖于父进程的某个行为,就可能导致死锁(这个概念自己了解吧)。
这里就不多讲fork和vfork的区别了,可以点击那里,是推荐的好的博客,去了解吧!
- exec:执行新进程。
这个函数族有太多的‘’兄弟姐妹‘’,还是有必要了解一下,我也没记住,再学习一遍吧:
#include<unistd.h>
int execve(const char * path ,char * const argv[],char *const envp[]);
//参数path是路径名,参数argv和参数envp和main函数中的参数对应
int execv(const char * path , char * const envp[]....);
//函数通过路径名调用可执行文件作为新进程映像
int execl(const char * path ,const char * arg ....);
//函数与execv函数用法类似,传参时每个命令行参数都声明为一个单独的参数
//使用“...”说明参数不止一个,参数以空指针作为结束,。
int execle(const char * path ,const char * arg ....);
//用法和楼上用法类似,只是要显示只是环境变量,位于空指针之后
int execlp(const char * file , const char *arg...);
//函数类似于execl,区别和excvo和execv一样
int execvp(const char * file ,char* const argv[] );
//函数用法与execv类似,不同的是参数filename。该参数如果包含“/”就相当于路径名,
//如果不包含,就会在环境变量中寻找
- exit :终止进程。exit(0)进程正常终止,其他都为异常终止。
- wait:(在后面介绍一些进程时会用到)将父进程挂起,等待子进程终止。
- nice:改变进程优先级
#include<unistd.h>
int nice(int increment){//改变调用进程的优先级
int oldpri = getpriority(PRIO_PROCESS,getpid());
return setpriority(PRIO_PROCESS,oldpro+increment);
}
#include<sys/resource.h>
int getpriority(int which,inr who);//获取进程优先级
int setpriority(int which ,int who);//设置指定进程的优先级
nice系统调用是是上面两个函数的组合
用例:
int old_pri = getpriority(PRIO_PROCESS,0);//返回一组进程的优先级
int newpri =nice(2);/
exit(0);
进程的内存映像
程序和进程的最关键区别就在于一动一静,二者却是密切相关的,程序转化为进程主要经过以下步骤:
- 内核将程序读入内存,为程序分配空间。
- 内核为进程分配标识PID和所需资源。
- 内核为进程保存PID和相应状态信息,把进程放在运行队列中等待系统调度执行。
进程内存映像主要包括
:
进程程序块,即被执行的程序,规定了进程一次运行应完成的功能。通常它是纯代码,作为一种系统资源可被多个进程共享。
进程数据块,即程序运行时加工处理对象,包括全局变量、局部变量和常量等的存放区以及开辟的工作区,常常为一个进程专用。
系统/用户堆栈,每一个进程都将捆绑一个系统/用户堆栈。用来解决过程调用或系统调用时的信息存储和参数传递。
进程控制块,每一个进程都将捆绑一个进程控制块,用来存储进程的标志信息、现场信息和控制信息。进程创建时,建立进程控制块;进程撤销时,回收进程控制块,它与进程一一对应。
这个是度娘下来的,以后方便看~~~~
可见每个进程有四个要素组成:控制块、程序块、数据块和堆栈。
几种比较典型进程
孤儿进程
如果一个子进程的的父进程先于子进程结束,子进程就会成为孤儿进程,他将由系统进程收养。成为系统进程的子进程。
创建:
pid_t pid;
pid =fork();
if(pid ==-1){
printf("创建进程失败!\n");
exit(-1);
}
else if(pid == 0){
printf("儿子进程正在跑!\n");
sleep(5);
}
else{
printf("父进程正在跑!\n");
exit(0);
}
return 0;
这个不难创建,就让子进程多跑一会儿,然后只要执行到父进程,就正常结束他。然后正在跑的子进程就成为了系统进程的子进程,通常可以用它创建守护进程。
僵尸进程
在创建子进程后,子进程先与父进程退出时,父进程没有调用wait或waitpid函数,子进程就会成为僵尸进程。
wait和waitpid函数如下:
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *statioc);//参数是子进程的退出码
pid_t waitpid(pid_t pid ,int statioc,int options);//options允许用户改变waitpid的行为,一般情况下为0,如果想让父进程周期性检查某个特定的子进程是否已经退出,可以这样做:
waipid(child_pid ,(int*)0,WNOHANG);//如果子进程已经结束,返回子进程ID调用
//失败返回-1.未退出返回0。
举个例子:
pid_t pid ;
int status;
pid =fork();
if(pid<0){
.....//你懂得
}
if(pid == 0){
pid_t pid2 ;
int status2;
pid2 = fork();//创建孙子进程
if(pid2 <0){
....
}
else if(pid2 ==0) {
.....
exit(0);
}
if(wait(pid2,&status,0)<0){等待孙子进程结束
//提示等待pid2号进程失败
}
}
exit(0);//退出子进程
if(wait(pid,&status)<0){
//报错
}
(注释:上面程序没套main函数)
所以创建进程是当子进程先结束时,记得调用wait函数,不然的话,耗费资源不说,时候自己程序bug,都不知怎么找,写程序尽量避免出现僵尸进程,我是吃过亏的!!!
守护进程
守护进程是指后台运行的,没有控制终端与之相连的进程,独立与终端,周期性的执行某一任务,通常linux大多数的服务器就是用守护进程实现的,守护进程是很有用的进程!!
创建:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <time.h>
#include <syslog.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信号
signal(SIGCHLD,SIG_IGN);
return 0;
}
int main()
{
time_t now;
init_daemon();
syslog(LOG_USER|LOG_INFO,"TestDaemonProcess! \n");
while(1) {
sleep(8);
time(&now);
syslog(LOG_USER|LOG_INFO,"SystemTime: \t%s\t\t\n",ctime(&now));
}
}
进程学习的更深层次的东西还很多,所以这儿可能就介绍了进程中的冰山一角而已,所以“路漫漫其修远兮,吾将上下而求索”,既然学习进程,这些是远远不够的,还是得坚持往深层次的研究。