Unix进程和线程管理及其异同
一,进程
1,什么是进程
在最初的单处理器系统中,系统中的多道程序按照一定规则切换而实现多任务处理,后来发现多个程序并发导致系统资源被共享,为了描述和管理程序对共享资源的使用情况,就提出了进程的概念。所以可以知道进程就是系统分配和管理资源的单位,打个不恰当的比喻,可以把系统比作老板,员工比作进程,老板发工资是按人头发的,计算的是每个人应得多少钱。
进程的定义众说纷纭,但含义基本类似,我看过一本说上给的定义是:进程是一个具有一定独立功能的程序或程序段在一组数据集合上的一次动态执行过程。
2,进程的描述
进程具有动态性,独立性,并发性等特征。进程中含有执行任务的程序、进程执行中表现的各种状态、程序执行中使用的数据及在某一时间点上各个寄存器中保留的信息。
为了便于进程的管理,在操作系统内部要建立进程的数据描述信息,进程的描述信息和进程实体构成了程序的结构,其中包括:
1)进程控制块(PCB)
PCB中包括了进程描述信息、控制信息、进程使用资源情况、处理器现场保护结构等。
PCB是进程动态特征的集中体现,操作系统是从进程PCB的信息感知进程的存在的,所以一个进程的PCB是常驻内存的,后两部分在进程未被调度时会存储在外存中。
2)程序段
包含了进程的程序代码。
3)数据结构集
是进程执行时要访问的工作区和数据对象。
3,进程控制的基本概念
1)原语
原语是完成特定功能的程序段,其执行具有不可分割、不可间断、不可并发等原子特性。操作系统中包括如进程创建、进程撤销、进程阻塞、进程唤醒等原语。
2)临界区
是指在共享某个资源时不予许进程交叉执行的一段程序,具有这种特性的资源称为临界资源。
3)进程互斥
即某个共享资源不允许多个进程交叉执行。
4)进程同步
在一组并发进程中,由于直接制约关系而相互发送消息、协同工作的过程称为进程同步。
5)信号量
对临界区中的共享资源设置的一种管理变量,该变量可以描述共享资源被使用和释放的状态。分为公有信号量和私有信号量,公有信号量被所有并发进程共享,私有信号量被多个相互制约的进程共享。
4,进程创建
使用系统调用fork()
来完成,该函数调用失败时返回-1,成功时在父进程中返回子进程的pid,在子进程中返回0。
这句话的意思不好理解,需要了解fork()函数的原理,fork()的原理就是把父进程的所有资源都复制一遍,并给复制出来的进程重新分配pid就是子进程了。并且复制出来的进程状态(变量的值、代码执行到的位置。。。)也和父进程执行到fork()这个语句是一样。所以fork()函数在父进程和子进程中都会返回一次。如果你在fork()后打印pid到屏幕会有两个值,分别是两个进程执行的结果。
代码如下:
int pid=fork();printf("%d\n",&pid);
那么这两行代码的执行结果就可能是下面这样的:
31980
fork系统调用完成的工作步骤如下
- 1)为子进程在进程表中分配一个空闲的proc结构
- 2)赋给子进程唯一的进程标识(pid)
- 3)复制父进程上下文的逻辑副本(这时对父进程中可用于共享的部分,如进程的正文段,并不是真正进行复制操作,而只是增加共享区的引用计数)
- 4)增加与父进程相关联的文件表和索引节点表的引用数
- 5)分别对父子进程返回pid(父进程返回大于0的整数,子进程返回0)
5,进程终止
通过系统调用exit()
来终止正在运行的进程,这时进程会释放其所占有的资源,如打开的所有文件、释放进程上下文占用的内存等,但该进程的proc仍保留在内存中,直到下一轮调度来临时才完成进程表项的调整。
孤儿进程
是指父进程已终止,而子进程还未终止的进程。Unix系统中孤儿进程由1号进程(init进程)来收养,因此所有孤儿进程的父进程都是1号进程。还规定,所有成为孤儿进程的进程都会变成后台进程。
僵死进程
指已经完成任务等待父进程将其回收的进程,在Unix中由父进程使用wait()系统调用进行回收。
6,exec系统调用
前面说fork会复制父进程,即父子进程会执行同一份代码,当然你可以根据父子进程的fork返回值不同的特点有条件语句使父子进程执行不同的代码,但是Unix提供了更为直接和方便的方法。
当用户调用exec系统调用后,操作系统会参数中指明的一个可执行程序的复本将调用程序的正文段和数据段进行覆盖,然后使用提供的参数去执行这段新代码。
实验
/*cp.c文件,功能是复制一个文件,源代码如下*/#include#include #include #include int main(int argc,char *argv[]){ int fd1,fd2,n; char buf[512]; if(argc<=2){ printf("ERR\n");exit(1); } fd1=open(argv[1],0);//read only fd2=creat(argv[2],0644); while((n=read(fd1,buf,512))>0) write(fd2,buf,n); close(fd1); close(fd2); return 0;}
使用gcc -o cp cp
命令编译以上代码,得到可执行文件cp,执行结果如下:
/*exec.c文件源代码如下*/#include#include int main(){ printf("One\n"); //execl("/bin/ls","ls",NULL); execl("./cp","","a.c","aa.c",NULL); printf("Two\n"); perror("err"); return 0;}
execl是exec的一种扩展形式,用法差不多,execl的第一个参数是可执行文件的路径,最后是一个字符指针数组表示执行程序的环境,一般默认用0就行,中间的用来传递给可执行文件的参数
执行结果:
可以看到printf("Two\n");
并没有被执行,因为程序后面的代码被cp可执行程序覆盖了,也可以看到cp执行的结果是没问题的。
二,线程
1,什么是线程
简单的说,由于进程的独立性和隔离性,导致在进程管理方面的开销比较大,所以引入了线程的概念,有人说线程是轻量型的进程,这样的观点可以比较好的形容出线程的“形状“或说包含的内容,从下面的进程和线程的描述图就可以看出这种观点的来源:
为什么需要线程?我理解出来就是一句话:我需要的只是一个并发执行的功能,且用不到整个进程的所有资源。
比如说我希望由一个额外的并发来在我执行其他操作时帮我执行数据库查询的工作,很明显这个并发工作只需要以下内容:
- 数据库的地址以及账号密码
- 一个可以用来执行查询函数的栈帧
若此时为这个查询操作创建一个新进程,并把父进程的所有数据拷贝过去,很明显会造成很多系统资源浪费。
所以我认为简单的说线程就是进程内分出来的可以单独执行的栈帧。
2,线程创建
一个不指定属性的线程是一个默认线程,默认线程具有如下属性:
线程是非绑定的
在系统中通常使用一个轻量进程池完成线程对LWP(轻量级进程)的映射,非绑定的含义是指线程可由系统映射到任何LWP中,而且线程可以在LWP中移动。
线程是未分离的
是指该线程有一个等待其结束的线程。
该线程具有一个默认堆栈区。
该线程具有和父线程一样的优先级。
POSIX线程创建:
int pthread_create(pthread_t *tid, const pthread_attr *tattr, void (start_routine)(void), void *arg);
参数说明:
tid 线程标识符
tattr 线程初始化的属性,设为NULL即为上述默认属性
start_routine 线程入口函数地址
arg 万能指针,说明自定义函数传参时,所分配的栈区位置
- 返回值 成功返回0,其他都表示出错,常见出错类型:
- EDGAIN:某个系统限制被超出,如创建太多LWP
- EINVAL:tattr有错误
- ENOMEM:没有创建线程所需内存
3,线程管理
1,分离线程
分离线程即代表一个线程结束后会被系统直接回收,而不必等待其他线程使用pthread_join()
进程回收。
ret=pthread_detach(tid);
返回值为0表示执行成功;EINVAL表示tid指明的线程不合法;ESRCH表示tid指明的线程不是一个未分离的线程。
2,等待线程结束
效果和进程中的wait()
相同,线程中函数原型为:
ret=pthread_join(tid,&status);
3,终止线程
进程中为exit(&status)
,线程中为pthread_exit(&status)
4,获取线程标识符
调用系统调用pthread_self()
返回值为当前线程标识符。
4,实验
原理是写一份批量文件复制程序,为每一个文件创建一个线程来实现文件复制功能(调用back_、up_file函数)。
#define _BACKFILE#include#include #include #include #include #include #include #include #include #define BUFFERSIZE 1024#define MAXBACKUPFILE 50#define MAXNAMESIZE 80void *backup_file(void *arg);/***************************************************************/int main(int argc,char *argv[]){ pthread_t thread_id[MAXBACKUPFILE];/*all thread_id*/ int filenum; char filename[MAXNAMESIZE]; int fd[MAXBACKUPFILE][2]; int *bytes_copied_p; int total_bytes_copied=0; filenum=argc-1; if(filenum>MAXBACKUPFILE){ fprintf(stderr, "USUAGE:%s arg1 arg2 ... arg%d\n", *argv,MAXBACKUPFILE);exit(1); } //copy every file //printf("begin to copy file\n\n"); int i; for(i=0;i 0){ bytes_written=write(des_file, bufp,bytes_read); if(bytes_written<0 && errno!=EINTR) break; else if(bytes_written<0) continue; *bytes_copied_p+=bytes_written; bytes_read-=bytes_written; bufp+= bytes_written; } } close(src_file); close(des_file); pthread_exit(bytes_copied_p);}