1. 进程
在此之前先理解一个概念:
在Unix/Linux系统中,正常情况下,子进程是由父进程创建的,但是子进程的结束与父进程的运行是一个异步过程,也就是说,父进程无法预测子进程的结束时间。当一个进程完成它的工作终止之后,父进程需要调用wait和waitpid获得子进程的最终状态,调用后才会释放子进程的剩余信息。
1.1 进程的含义
进程是并发执行的程序在执行过程中分配和管理资源的基本单位。
1.2 进程的三种状态
- 就绪态:具备了一切运行需要的条件,由于其他进程占用CPU而暂时无法运行
- 运行态:获得CPU的进程处于此状态,对应的程序在CPU上运行着
- 阻塞态:为了等待某个外部事件的发送,暂时无法运行
1.3 进程控制块(PCB)
PCB是进程的唯一标识。进程创建时,为该进程生成一个PCB,进程终止时,回收PCB。
PID
每个进程都有唯一的PID,但是PID是可以重用的。当进程终止时,其他创建的新进程可能会使用该PID。
PID=0:该进程属于系统进程。
PID=1:该进程为普通用户进程,以超级用户特权运行。
PID=2:守护进程,负责支持虚拟存储系统的分页操作
1.4 进程的创建
先新建一个PCB,然后将父进程的PCB复制到新建的PCB中,再修改部分数据,分配新的内核堆栈,新的PID等,最后将PCB添加到node节点下,也就是链表中。(PCB是由链表实现的)
1.5 与进程有关的几个函数 fork,vfork
1.5.1 fork
fork函数时调用一次,返回两次。在父进程和子进程中各调用一次。子进程中返回值为0,父进程中返回值为子进程的PID。程序员可以根据返回值的不同让父进程和子进程执行不同的代码。
int main()
{
pid_t pid;
char *message;
int n = 0;
pid = fork();
while(1){
if(pid < 0){
perror("fork failed\n");
exit(1);
}
else if(pid == 0){
n--;
printf("child's n is:%d\n",n);
}
else{
n++;
printf("parent's n is:%d\n",n);
}
sleep(1);
}
exit(0);
}
上面程序执行的结果如下
可以发现最后child比parent先调用了,且父进程和子进程各自的变量不相互影响。
一般来说,fork之后父、子进程执行顺序是不确定的,这取决于内核调度算法。进程之间实现同步需要进行进程通信。
什么时候使用fork
一个父进程希望子进程同时执行不同的代码段,这在网络服务器中常见——父进程等待客户端的服务请求,当请求到达时,父进程调用fork,使子进程处理此请求。
1.5.1 vfork
它与fork相同的地方就是它们的返回值相同。但是fork创建子进程的时候会将父进程的数据空间、堆栈复制一份。而vfork创建子进程,与父进程内存数据共享。并且vfork保证子进程先执行,只有在子进程调用exit()或者exec后,父进程才往下执行
int main()
{
pid_t pid;
char *message;
int n = 0;
int i;
pid = vfork();
for(i = 0; i < 10; i++){
if(pid < 0){
perror("fork failed\n");
exit(1);
}
else if(pid == 0){
n--;
printf("child's n is:%d\n",n);
if(i == 1)
_exit(0);
//return 0;
//exit(0);
}
else{
n++;
printf("parent's n is:%d\n",n);
}
sleep(1);
}
exit(0);
}
上面程序的运行结果如下:
可以发现子进程先被执行,exit后,父进程才被执行,同时子进程改变了父进程中的数据
如果上面程序的子进程return 0会发生什么
如果你在子进程中return,那么基本是下面的过程: 1)子进程的main() 函数 return了,于是程序的函数栈发生了变化。 2)而main()函数return后,通常会调用 exit()或相似的函数(如:_exit(),exitgroup()) 3)这时,父进程收到子进程exit(),开始从vfork返回,但是此时栈已经发生变化(其实就是刚才存在的栈被弹出了)
1.6 僵尸进程
当父进程用fork创建子进程的时候,如果子进程退出,父进程没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符会依旧保存在系统中,那么这种进程就叫做僵尸进程。
僵尸进程的危害
如果进程不调用wait和waitpid的话,子进程剩余的信息就不会释放,它的进程号会一直被占用。但是操作系统分配的进程号又是有限的,如果有大量的僵尸进程,那么就有大量的进程号无法使用,这样可能会导致无法新建进程。
1.7 孤儿进程
如果父进程先退出,它的子进程还在运行,那么这些子进程就会变为孤儿进程,会被init进程收养,后面它们的父进程就是init进程。
1.8 守护进程
守护进程其实也是一个孤儿进程,它的父进程就是init进程。
守护进程是运行在后台的一种特殊进程,它独立于控制终端并且周期性的执行某种任务或等待处理某些待发生的事件。它不需要用户输入就能运行并且提供服务。
守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都在运行,守护进程经常以超级用户(root)权限运行。
常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。
2. 线程
在网络或多用户的环境下,一个服务器往往要接收大量且不确定数量的用户的并发请求,如果给每一个用户都开一个进程,显然是不可能的,所以就引入了线程的概念。
2.1 线程的含义
单个线程的进程是单线程。
线程共享进程的地址空间,线程是处理器调度的基本单位。
线程私有和共享哪些资源
- 线程私有:线程栈,寄存器,程序计数器
- 线程共享:堆,地址空间,全局变量,静态变量
2.2 线程的同步与互斥
同步:多个线程为了合作完成某任务,按照先来后到的顺序执行
互斥:线程中的某些资源,一次只能允许一个线程进行访问,在这个线程进行访问的期间,其他线程不能访问该资源。
多线程同步和互斥的实现方法
互斥量 :只有01两个值
读写锁
临界区
信号量:信号量在计数大于0时表示触发状态,等于0表示资源已经耗尽,故信号量处于末触发。在对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。
条件变量
事件:用来通知线程有一些事件已发生,从而启动后继任务的开始
多线程的同步机制
临界区:不可以跨进程,忘记解锁会无限等待,要么存在要么没有,多线程访问独占性共享资源
互斥量:可以跨进程,忘记解锁会自动释放,要么存在要么没有
事件:又叫线程触发器,不可以跨进程,要么存在要么没有,一个线程来唤醒另一个线程(包括自动和人工两种方式)
信号量:可以跨进程,始终代表可用资源数量,当资源数为0时,线程阻塞,允许多个线程同时访问一个共享资源