进程控制编程
进程:进程是一个具有一定独立功能的程序的一次运行活动,同时也是资源分配的最小单元;
进程 | 程序 |
---|---|
程序执行的实例 | 放到磁盘的可执行文件 |
进程不可在计算机之间迁移 | 程序通常对应着文件、静态和可以复制 |
动态 | 静态 |
暂时:进程是一个状态变化的过程 | 长久:程序可长久保存 |
进程与程序组成不同:进程的组成包括程序、数据和进程控制块(即进程状态信息)
进程与程序的对应关系:通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包括多个程序。
进程的生命周期
创建: 每个进程都是由其父进程创建,进程可以创建子进程,子进程又可以创建子进程的子进程
运行: 多个进程可以同时存在,进程间可以通信
撤销: 进程可以被撤销,从而结束一个进程的运行
进程的状态(运行):
执行状态:进程正在占用CPU
就绪状态:进程已具备一切条件,正在等待分配CPU的处理时间片
等待状态:进程不能使用CPU,若等待事件发生则可将其唤醒
Linux进程
Linux系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。
也就是说,每个进程都是一个独立的运行单位,拥有各自的权利和责任。其中,各个进程都运行在独立的虚拟地址空间,因此,即使一个进程发生异常,它也不会影响到系统中的其他进程。
进程ID:(PID)标准进程的唯一数字
父进程:(PPID)
启动进程的用户ID(UID)
进程互斥:进程互斥是指当有若干进程都要使用某一共享资源时,任何时刻最多允许一个进程使用,其他要使用该资源的进程必须等待,直到占用该资源者释放了该资源为止。
临街资源:(共享资源)一次只允许一个进程访问的资源称为临界资源
临界区:进程中访问临界资源的那段程序代码称为临界区
进程同步: 一组并发进程按一定的顺序执行的过程称为进程间的同步,具有同步关系一组并发进程称为合作进程,合作进程间互相发送的信号称为消息或事件。
进程调度:按一定算法,从一组待运行的进程中选出一个来占有CPU运行。
调度方式:抢占(优先级) 、非抢占式
算法:
- 先来先服务调度算法
- 短进程优先调度算法
- 高优先级优先调度算法
- 时间片轮转法
目前,后两者最为常用。
死锁:多个进程因竞争资源而形成一种僵局,若无外力作用,这些进程都将永远不能再向前推进
获取ID
pid_t getpid(void) 获取进程ID
pid_t getppid(void) 获取父进程ID
例如:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("PID = %d\n", getpid());
printf("PPID = %d\n", getppid());
while(1);
return 0;
}
进程创建
1.fork()
pid_t fork(void) 创建子进程
子进程的ID号为父进程的ID号 + 1
特别:返回值返回两次,父进程返回子进程的ID号,子进程返回0
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
pid = fork();
if(-1 == pid)
{
perror("fork");
exit(1);
}
else if(0 == pid) //子进程返回0,子进程执行部分
{
printf("Child Process Id = %d\nParent Process Id = %d\n", getpid(), getppid());
}
else //父进程执行部分
{
printf("Parent Process Id = %d\n", getpid());
}
return 0;
}
思考:下面这段代码的最终输出结果是???
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
int count = 0;
count++;
printf("%d\n", count);
return 0;
}
注意:使用fork()创建进程后,会有两个进程存在,对于fork而言,这两个进程在不同的地址空间(之前我们讲过对于一个进程而言会有一个4G的虚拟内存),那么对于fork后的两个进程,会存在两个地址一模一样的空间,子进程会将父进程的所有代码(除去各自独有的代码)复制到自己的代码段。
写时复制:当某个进程访问某个变量,并要修改时,则会分配另一个空间。
对于上面这个情况,当子进程尝试count++时,会改变count的值,此时系统会开辟另一个空间给子进程,此时count为0,那么经过自加后,输出count为1;对于父进程而言,也是一样的。
所以,上面的代码最终结果是:
1
1
2.vfork()
pid_t vfork(void);
- vfork的子进程必须加exit()退出,否则会出错;
- 子进程先运行,子进程运行后,再执行父进程
- vfork的子进程与父进程共享相同的资源
下面这段代码的输出结果又是如何的呢?
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
int count = 0;
pid = vfork();
if(-1 == pid)
{
perror("vfork");
exit(1);
}
else if(0 == pid)
{
count++;
printf("count = %d\n", count);
exit(1);
}
else
{
count++;
printf("count = %d\n", count);
}
return 0;
}
我们知道,vfork的子进程和父进程共享相同的资源,那么在执行子进程中的count++时,count由原来的0变成1,输出1;
执行父进程的count++时,count又由1变成2,输出2;
所以最终结果是:
1
2
如果我将程序改动如下,结果又是什么呢?
......
int main()
{
pid_t pid;
pid = vfork();
int count = 0;
......
}
将pid = vfork();与int count = 0;的位置交换,最终结果会是如何?
结果如下:
1
1
为什么呢?
此时子进程和父进程依旧共享相同的资源,但它也共享了”int count = 0”这句话,所以在子进程执行完毕后,count = 1;当父进程开始执行时,先执行的是int count = 0,将原来的1又置为0,再经过自加操作,那么输出结果还是1;
3.exec函数族
exec启动一个新程序,替换原有的进程,因此进程的PID不会改变
1.int execl(const char *path, const char *arg, …);
path:被执行程序名(含完整路径)。
arg: 被执行程序所需的命令行参数,含程序名。以空指针(NULL)结束。
例如:
使用execl在进程中调用ls,显示当前目录下的文件信息。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = vfork();
if(-1 == pid)
{
perror("vfork");
exit(1);
}
else if(0 == pid)
{
printf("Child process:%d\n", getpid());
execl("/bin/ls",NULL);
}
else
{
printf("Parent process\n");
}
return 0;
}
2.int execv(const char *path, char *const argv[]);
参数:
path:被执行程序名(含完整路径)。
argv[]: 被执行程序所需的命令行参数数组。
例如:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
char *argv[] = {"ls",NULL};
execv("/bin/ls", argv);
return 0;
}
进程等待
先介绍两个概念,一个是孤儿进程,一个是僵尸进程。
孤儿进程:其父进程在它之前结束,不能再对它进行回收
僵尸进程:子进程结束后,没有被回收的时间段内,被称为僵尸进程。
1.pid_t wait(int *status);
功能:阻塞该进程,直到其某个子进程退出。
例如:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
int status;
if(-1 == pid)
{
perror("fork");
exit(1);
}
else if(0 == pid)
{
sleep(2);
printf("Child Process\n");
exit(6);
}
else
{
printf("Parent Process\n");
wait(&status); //如果不需要知道它的退出类型,wait的形参写NULL也可以
if(WIFEXITED(status))
{
printf("Exit Normally %d\n", WEXITSTATUS(status));
}
}
return 0;
}
如果不写wait(),在子进程sleep的过程中,其父进程就已经结束了,那么子进程就会变成孤儿进程,但加上wait后,父进程直到子进程结束才会退出。
...
else if(0 == pid)
{
printf("Child Process\n");
exit(6);
}
else
{
sleep(2);
printf("Parent Process\n");
wait(&status);
if(WIFEXITED(status))
{
printf("Exit Normally %d\n", WEXITSTATUS(status));
}
}
...
如果我们将子进程中的sleep函数放在父进程中,那么在子进程执行结束后,父进程需要沉睡两秒后才执行,这两秒内,父进程没有去回收子进程,所以称子进程为僵尸进程,但两秒钟后,子进程又被回收。
2.pid_t waitpid(pid_t pid, int *status, int options);
参数pid:(欲等待的子进程识别码)
pid < -1: 等待进程组识别码为pid绝对值的任何子进程。
pid = -1: 等待任何子进程,相当于wait()。
pid = 0: 等待进程组识别码与目前进程相同的任何子进程。
pid > 0: 等待任何子进程识别码为pid的子进程。
参数option:(通常设置为0)
WNOHANG: 如果没有任何已经结束的子进程则马上返回,不予以等待。
WUNTRACED :如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会。
进程退出
exit()与_exit()
exit():在停止进程之前,要检查文件的打开情况,并把文件缓冲区中的内容写回文件才停止进程。
_exit():直接使进程停止,清除其使用的内存,并清除缓冲区中内容
所以,我们通常选择使用exit(),而不是用_exit();