Linux编程——进程与线程

(一) 理论部分

1.进程与线程

进程

进程(process)是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

进程本质上是正在执行的一个程序,是容纳运行一个程序所需要所有信息的容器。与一个进程相关的是进程的地址空间(address space)和进程表(process table)。进程的地址空间包括代码段、数据段、堆栈段。下面画出了进程的三种状态,以及状态之间的切换:

在多任务系统中,CPU使用某种调度算法在不同进程间来回快速切换,就好像这几个进程在并发执行一样。进程间的切换是通过中断(interrupt)来实现的。OS维护一张表格,即进程表,每个进程占用一个进程表项(也称进程控制块),该表项包含了进程状态的重要信息,包括程序计数器(保存了下一条指令的内存地址)、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。

系统还对进程区分了不同的状态.将进程分为新建,运行,阻塞,就绪和完成五个状态.

新建表示进程正在被创建,

运行是进程正在运行,

阻塞是进程正在等待某一个事件发生,

就绪是表示系统正在等待 CPU 来执行命令,

而完成表示进程已经结束了系统正在回收资源.

线程

线程是进程的一个实体,CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

线程是把进程的两项功能——独立分配资源与被调度分派执行分离开来。

2.关系

一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。

进程作为系统资源分配和保护的独立单位,不需要频繁地切换。线程作为系统调度和分派的基本单位,能轻装运行,会被频繁地调度和切换。

 

3.区别

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

l  简而言之,一个程序至少有一个进程,一个进程至少有一个线程.

l  线程的划分尺度小于进程,使得多线程程序的并发性高。

l  另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

l  线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

l  从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

 

 


 

(二)         实际应用

2.1 进程编程

(1) 获得进程ID

系统调用getpid 可以得到进程的ID,getppid 可以得到父进程(创建调用该函数进程的进程)ID.调用getuid 可以得到进程的所有者的ID.调用geteuid 我们可以得到进程的有效用户ID.这个ID 和系统的资源使用有关,涉及到进程的权限.和用户ID
相对应进程还有一个组ID 和有效组ID 系统调用getgid getegid 可以分别得到组ID 和有效组ID

 

(2)创建进程

当计算机开机的时候,内核(kernel)只建立了一个init进程,剩下的所有进程都是init进程通过fork机制建立的

创建进程要调用fork 函数

一个进程调用了fork 以后,系统会创建一个子进程.这个子进程和父进程不同的地方只有他的进程ID 和父进程ID,其他的都是一样.

fork 调用失败的时候(内存不足或者是用户的最大进程数已到)fork 返回-1,否则fork 的返回值有重要的作用.

对于父进程fork 返回子进程的ID而对于子进程fork返回0就是根据这个返回值来区分父子进程的

这个函数会有两次返回,将子进程的PID返回给父进程,0返回给子进程。

当进程fork的时候,Linux在内存中开辟出一片新的内存空间给新的进程,并将老的进程空间中的内容复制到新的空间中,此后两个进程同时运行。

老进程成为新进程的父进程(parent process),而相应的,新进程就是老的进程的子进程(child process)。一个进程除了有一个PID之外,还会有一个PPID(parent PID)来存储的父进程PID。如果我们循着PPID不断向上追溯的话,总会发现其源头是init进程。所以说,所有的进程也构成一个以init为根的树状结构。

 

父进程创建子进程的目的:

进程为了早一点完成任务就创建子进程来争夺资源. 一旦子进程被创建,父子进程一起从fork 处继续执行,相互竞争系统的资源.

 

pid = fork()

创建与父进程相同的子进程

pid = waitpid(pid, &statloc, options)

等待一个进程终止

s = execve(name, argv, environp)

替换一个进程的地址空间

exit(status)

中止进程执行并返回状态

s = kill(pid, signal)

发送信号给一个进程

 

子进程继续执行,而父进程阻塞直到子进程完成任务.这个时候我们可以调用wait 或者waitpid 系统调用.

 

(3)进程的终结

进程有5种正常终止方式

l  在main函数内执行return语句,等效于调用exit

l  调用exit函数

l  调用_exit_Exit函数

l  进程的最后一个线程在其启动例程中执行返回语句

l  进程的最后一个线程调用pthread_exit函数

讲到exit这个系统调用,就要提及另外一个系统调用_exit_exit()函数位于unistd.h中,相比于exit()_exit()函数的功能最为简单,直接终止进程的运行,释放其所使用的内存空间,并销毁在内存中的数据结构,而exit()在于在进程退出之前要检查文件的状态,将文件缓冲区中的内容写回文件。

 

当子进程终结时,它会通知父进程,并清空自己所占据的内存,并在kernel里留下自己的退出信息(exit code,如果顺利运行,为0;如果有错误或异常状况,为>0的整数)。在这个信息里,会解释该进程为什么退出。父进程在得知子进程终结时,有责任对该子进程使用wait系统调用。这个wait函数能从kernel中取出子进程的退出信息,并清空该信息在kernel中所占据的空间。但是,如果父进程早于子进程终结,子进程就会成为一个孤儿(orphand)进程。孤儿进程会被过继给init进程init进程也就成了该进程的父进程。init进程负责该子进程终结时调用wait函数。

当然,一个糟糕的程序也完全可能造成子进程的退出信息滞留在kernel中的状况(父进程不对子进程调用wait函数),这样的情况下,子进程成为僵尸(zombie)进程。当大量僵尸进程积累时,内存空间会被挤占

 

(4)进程的守护

后台进程的创建思想:首先父进程创建一个子进程.然后子进程杀死父进程. 信号处理所有的工作由子进程来处理.

 


 

2.2 线程编程

(1)线程标识

线程的ID

线程ID只在它所属的进程环境中有效。

数据类型:pthread_t

对两个线程的ID进行比较必须采用函数

可以通过调用pthread_self函数获得自身的线程ID

(2)常用函数

 

#include

int pthread_equal(phtread_t tid1, phtread_t tid2 );

返回值:若相等则返回非0值,否则返回0

 

pthread_t pthread_self(void);

返回值:调用线程的线程ID

 

线程的创建函数:

int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

pthread_create 创建一个线程 thread 是用来表明创建线程的 ID,attr 指出线程创建时候的属性,我们用 NULL 来表明使用缺省属性.

 

线程终止

void pthread_exit(void *retval);

pthread_exit 函数和 exit 函数类似用来退出线程.这个函数结束线程,释放函数的资源,并在最后阻塞,直到其他线程使用 pthread_join 函数等待它.然后将*retval 的值传递给**thread_return.

 

等待线程

int pthread_join(pthread *thread,void **thread_return);

 

由于一个进程中的多个线程是共享数据段的,通常在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放

 

  •pthread_join()函数

    类似进程的wait()/waitpid()函数,用于将当前线程挂起来等待线程的结束

    是一个线程阻塞的函数,调用它的线程一直等待到被等待的线程结束为止

    函数返回时,被等待线程的资源就被收回

 

取消线程

int pthread_cancel (pthread_t id);

id:要取消的线程的ID,成功返回0,出错返回错误码。

  在别的线程中要终止另一个线程pthread_cancel()函数

  被取消的线程可以设置自己的取消状态

    –被取消的线程接收到另一个线程的取消请求之后,是接受还是忽略这个请求

    –如果接受,是立刻进行终止操作还是等待某个函数的调用等

 

(3)线程的同步

 

线程共享进程的资源和地址空间,对这些资源进行操作时,必须考虑线程间同步与互斥问题

  三种线程同步机制

u  互斥锁

u  信号量

u  条件变量

互斥锁更适合同时可用的资源是惟一的情况

信号量更适合同时可用的资源为多个的情况

【3.1】 互斥锁

  用简单的加锁方法控制对共享资源的原子操作

  只有两种状态: 上锁、解锁

可把互斥锁看作某种意义上的全局变量

  在同一时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作

  若其他线程希望上锁一个已经被上锁的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止

互斥锁保证让每个线程对共享资源按顺序进行原子操作

 

互斥锁分类

      区别在于其他未占有互斥锁的线程在希望得到互斥锁时是否需要阻塞等待

  快速互斥锁

    调用线程会阻塞直至拥有互斥锁的线程解锁为止

    默认为快速互斥锁

  检错互斥锁

    为快速互斥锁的非阻塞版本,它会立即返回并返回一个错误信息

 

互斥锁主要包括下面的基本函数:

  互斥锁初始化:pthread_mutex_init()

  互斥锁上锁:pthread_mutex_lock()

  互斥锁判断上锁:pthread_mutex_trylock()

  互斥锁解锁:pthread_mutex_unlock()

  消除互斥锁:pthread_mutex_destroy()

 

【3.2】 信号量

  操作系统中所用到的PV原子操作,广泛用于进程或线程间的同步与互斥

l  本质上是一个非负的整数计数器,被用来控制对公共资源的访问

  PV原子操作:对整数计数器信号量sem的操作

l  一次P操作使sem减一,而一次V操作使sem加一

l  进程(或线程)根据信号量的值来判断是否对公共资源具有访问权限

  –当信号量sem的值大于等于零时,该进程(或线程)具有公共资源的访问权限

  –当信号量sem的值小于零时,该进程(或线程)就将阻塞直到信号量sem的值大于等于0为止

 

PV操作主要用于线程间的同步和互斥

  互斥,几个线程只设置一个信号量sem

  同步,会设置多个信号量,安排不同初值来实现它们之间的顺序执行

 

信号量函数

  sem_init() 创建一个信号量,并初始化它

  sem_wait()sem_trywait(): P操作,在信号量大于零时将信号量的值减一

    •区别: 若信号量小于零时,sem_wait()将会阻塞线程,sem_trywait()则会立即返回

  sem_post(): V操作,将信号量的值加一同时发出信号来唤醒等待的线程

  sem_getvalue(): 得到信号量的值

  sem_destroy(): 删除信号量

 

 

 

备注

在命令后面加上&符号SHELL 就会把我们的程序放到后台去运行的.

l  进程间通信(IPCinter process communication

l  线程和子进程区别的经典阐述

子进程是通过拷贝父进程的地址空间来执行的.而线程是通过共享程序代码来执行的,讲的通俗一点就是线程的相同的代码会被执行几次.使用线程的好处是可以节省资源,由于线程是通过共享代码的,所以没有进程调度那么复杂。

 

 

实例:

/************************************

生成子进程

************************************/

#include

#include

#include

 

void print_exit()

{

         printf("the exit pid : %d \n",getpid());

}

 

int main()

{

         pid_t pid;

         atexit( print_exit );   //函数名在用于非函数调用的时候,都等效于函数指针

                   pid = fork();

                   if(pid < 0)

                            {

                            printf("error in fork!");

                            }

                   else if (0 == pid)

                            {

                            printf("I am the child process, my process's id is %d\n", getpid());

                            }

                   else

                            {

                            printf("I am the parent process, my process's id is %d\n", getpid());

                            sleep(2);

                            wait();

                            }

}

运行结果:


 

/***************************************

多线程程序

***************************************/

#include

#include

#include

#include

#include

 

 

void* task1(void*);

void* task2(void*);

 

 

void usr();

int p1,p2;

 

int main()

{

    usr();

    getchar();

    return 1;

}

 

 

 

void usr()

{

       pthread_t pid1, pid2;

       pthread_attr_t attr;

       void *p;

       int ret=0;

       pthread_attr_init(&attr);         //初始化线程属性结构

       pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);   //设置attr结构为分离

       pthread_create(&pid1, &attr, task1, NULL);         //创建线程,返回线程号给pid1,线程属性设置为attr的属性,线程函数入口为task1,参数为NULL

       pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

       pthread_create(&pid2, &attr, task2, NULL);

//前台工作

 

       ret=pthread_join(pid2, &p);         //等待pid2返回,返回值赋给p

       printf("after pthread2:ret=%d,p=%d\n", ret,(int)p);          

 

}

 

void* task1(void *arg1)

{

    printf("task1/n");

    pthread_exit( (void *)1);

 

}

 

void* task2(void *arg2)

{

    int i=0;

    printf("thread2 begin.\n");

    pthread_exit((void *)2);

}


运行结果截图

Linux编程——进程与线程_第1张图片

你可能感兴趣的:(Linux)