AI智能
改变未来

Linux学习-3进程与线程

目录

1 进程在内核中的组织

1.1 进程在内核的组织形式:进程控制块(PCB)(初始化信息)

1.1.1 Linux进程控制块:task_struct结构

1.1.2 task_struct:进程状态

1.1.3 task_struct:文件管理

1.1.4 task_struct:内存管理

1.1.5 进程在内核中的组织形式

2 进程属性

2.1 进程基本属性-进程ID

2.2 真实用户和有效用户的关系

2.3 有效用户和真实用户实例

2.4 普通用户能够修改自己的密码的原因

3 进程生命周期

3.1 进程的启动

3.2 进程的终止

3.3 进程的生命周期

3.3.1 终止进程的函数

3.3.2 exit与return的区别

3.3.3 注册终止处理函数

4 进程环境

4.1 进程内存空间布局

4.1.1 命令行参数

4.1.2 C程序中main函数参数

4.1.3 环境变量表

4.1.4 设置环境变量(putenv、setenv、unsetenv)

5 创建进程

5.1 创建进程

5.2 创建子进程

5.2.1 创建子进程示例

5.2.2 fork函数工作流程

5.2.3 fork函数执行后父子进程的主要异同

5.2.4 父子进程共享文件

5.2.5 fork的用法

5.2.6 vfork函数

6 获知子程序运行状态改变

6.1 僵尸进程

6.2 wait函数

6.3 waitpid函数

7 在进程中运行可执行文件

7.1 exec系列函数

7.2 execl函数

7.3 execv函数

7.4 execle函数

7.5 其他exec函数

 

7.6 exec类函数之间的关系

8 Linux线程控制

8.1 线程的基本概念

8.2 线程与进程的对比

8.3 线程ID

8.4 线程的创建

8.5 线程的终止

8.6 取消线程

8.7 父线程等待子线程终止

8.7.1 pthread_join函数

1 进程在内核中的组织

CPU载入程序到内存—>CPU初始化程序—>CPU进程调度程序

1.1 进程在内核的组织形式:进程控制块(PCB)(初始化信息)

初始化:进程ID,用户ID,进程状态,调度信息,文件管理,虚拟内存管理,信号,时间和定时器…

1.1.1 Linux进程控制块:task_struct结构

1.1.2 task_struct:进程状态

1.1.3 task_struct:文件管理

1.1.4 task_struct:内存管理

1.1.5 进程在内核中的组织形式

2 进程属性

2.1 进程基本属性-进程ID

2.2 真实用户和有效用户的关系

2.3 有效用户和真实用户实例

2.4 普通用户能够修改自己的密码的原因

3 进程生命周期

3.1 进程的启动

◼C程序的启动函数是main,也是进程代码的入口点

  • main ( int argc, char *argv[] );

◼当内核启动C程序时,会在调用main函数前调用特殊的启动函数来获取main函数地址和传递给main函数的参数,并且将这些信息填写到进程控制块中

3.2 进程的终止

正常终止

  • 从main函数中返回
  • 在任意代码中调用exit函数或_exit函数
  • 最后一个线程从其启动例程中返回
  • 最后一个线程调用pthread_exit函数

异常终止

  • 在任意代码中调用abort函数
  • 接收到终止信号

3.3 进程的生命周期

3.3.1 终止进程的函数

◼头文件stdlib.h ,函数定义:void exit( int status )◼头文件unistd.h,函数定义:void _exit (int status )◼调用这两个函数均会正常地终止一个进程◼调用_exit 函数将会立即返回内核◼调用exit 函数:

  • 执行预先注册的终止处理函数
  • 执行文件I/O操作的善后工作,使得所有缓冲的输出数据被更新到相应的设备
  • 返回内核

3.3.2 exit与return的区别

  • return是C语言关键字,exit是POSIX API函数
  • 在main函数中,执行return和调用exit函数会产生相同的效果
  • 在子函数中,执行return仅仅从子函数中返回,而调用exit函数将会退出当前进程 

3.3.3 注册终止处理函数

  1. 当进程终止时,程序可能需要进行一些自身的清理工作,如日志登记、资源释放等
  2. 通过atexit函数或on_exit函数允许进程注册若干终止处理函数,当进程终止时,这些终止处理函数将会被自动调用 

  ◼头文件stdlib.h

  • int atexit(void (*func)(void));
  • int on_exit (void (*func)(int, void *), void *arg);

◼ANSI C规定一个进程最多能注册32个终止处理函数◼当显示调用或者隐含调用exit函数终止进程(从main中返回、最后一个线程退出等)将会回调这些注册的终止处理函数(最先注册的函数最后被回调)◼显示调用_exit函数终止进程时将不会回调这些注册的终止函数  示例代码:atexit示例代码:on_exit 

4 进程环境

4.1 进程内存空间布局

正文:CPU执行的代码部分,正文段通常是共享、只读的

初始化的数据:包含了程序中需明确赋初值的变量,如全局变量int maxcount=99

未初始化的数据:程序执行之前,将此段中的数据初始化为0,如全局变量long sum[1000]

栈/堆:用于动态分配内存

命令行参数和环境变量:主要用于支撑函数调用存放参数、局部变量等

4.1.1 命令行参数

ls [参数] <路径或文件名>

  • ls –l /home

mkdir [参数] <目录名>

  • mkdir -p /home/xiaokun/src

cp [参数] <源文件路径> <目标文件路径>

  • cp –r /usr/local/src /root

4.1.2 C程序中main函数参数

4.1.3 环境变量表

 ◼每个进程都会有自己的环境变量表◼通过全局的环境指针(environ)可以直接访问环境变量表(字符串数组)  

  • 头文件unistd.h
  • extern char **environ;

◼环境变量字符串形式为“name=value”,name是环境变量名称,value为环境变量赋值

4.1.4 设置环境变量(putenv、setenv、unsetenv)

设置环境变量的三种方法:

  • putenv
  • setenv
  • unsetenv

putenv函数将环境变量字符串放入环境变量表中;若该字符串已经存在,则覆盖

  • 头文件:stdlib.h
  • int putenv(char *str);

 ◼setenv

  • 头文件:stdlib.h
  • int setenv(const char* name,const char* value, int rewrite);
  • setenv将指定环境变量的值设置为参数指定值(更改环境变量字符串)
  • 若name已经存在 ,rewrite不等于0时,则删除其原先的定义; rewrite等于0,则不删除其原先的定义。

unsetenv

  • 头文件:stdlib.h
  • int unsetenv(const char* name);
  • 删除指定的环境变量字符串

5 创建进程

5.1 创建进程

◼Linux中创建进程的方式:

  • 在shell中执行命令或可执行文件(由shell进程调用fork函数创建子进程)
  • 在代码中(已经存在的进程中)调用fork函数创建子进程 (通过fork函数创建的进程为已经存在进程的子进程)

◼Linux系统中进程0(PID=0)是由内核创建,其他所有进程都是由父进程调用fork函数所创建的◼Linux系统中进程0在创建子进程(PID=1,init进程)后,进程0就转为交换进程或空闲进程◼进程1(init进程)是系统中其他所有进程的共同祖先

5.2 创建子进程

函数原型

  • 头文件:unistd.h
  • pid_t fork(void);

返回值 ⚫fork函数被正确调用后,将会在子进程中和父进程中分别返回!!

  • 在子进程中返回值为0(不合法的PID,提示当前运行在子进程中)
  • 在父进程中返回值为子进程ID(让父进程掌握所创建子进程的ID号)

⚫出错返回-1

5.2.1 创建子进程示例

5.2.2 fork函数工作流程

◼子进程是父进程的副本

  • 子进程复制/拷贝父进程的PCB、数据空间(数据段、堆和栈)
  • 父子进程共享正文段(只读)

◼子进程和父进程继续执行fork函数调用之后的代码◼为了提高效率,fork后不并立即复制父进程数据段、堆和栈,采用了写时复制机制(Copy-On-Write)

  • 当父子进程任意之一要修改数据段、堆、栈时,进行复制操作,并且仅复制修改区域

5.2.3 fork函数执行后父子进程的主要异同

5.2.4 父子进程共享文件

◼父子进程对共享文件的常见处理方式

  • 父进程等待子进程完成。当子进程终止后,文件当前位置已经得到了相应的更新
  • 父子进程各自执行不同的程序段,各自关闭不需要的文件

5.2.5 fork的用法

父进程希望复制自己(共享代码,复制数据空间),但父子进程执行相同代码中的不同分支

  • 网络服务程序中,父进程等待客户端的服务请求,当请求达到时,父进程调用fork创建子进程处理该请求,而父进程继续等待下一个服务请求到达

父子进程执行不同的可执行文件(父子进程具有完全不同的代码段和数据空间)

  • 子进程从fork返回后,立即调用exec类函数执行另外一个可执行文件

5.2.6 vfork函数

◼vfork用于创建新进程,而该新进程的目的是执行另外一个可执行文件◼由于新程序将有自己的地址空间,因此vfork函数并不将父进程的地址空间完全复制到子进程中◼子进程在调用exec或exit之前,在父进程的地址空间中运行◼vfork函数保证子进程先执行,在它调用exec或者exit之后,父进程才会继续被调度执行(父进程处于TASK_UNINTERRUPTIBLE状态)

6 获知子程序运行状态改变

 ◼当一个进程发生特定的状态变化(进程终止、暂停以及恢复)时,内核向其父进程发送SIGCHLD信号◼父进程可以选择忽略该信号,也可以对信号进行处理(默认处理方式为忽略该信号)◼wait或waitpid函数可以用于等待子进程状态信息改变,并获取其状态信息  

6.1 僵尸进程

◼进程在退出之前会释放进程用户空间的所有资源,但PCB等内核空间资源不会被释放

  • 当父进程调用wait或waitpid函数后,内核将根据情况关闭该进程打开的所有文件,释放PCB(释放内核空间资源)
  • 对于已经终止但父进程尚未对其调用wait或waitpid函数的进程( TASK_ZOMBIE状态),称为僵尸进程

◼如果父进程在子进程终止之前终止,则子进程的父进程将变为init进程,保证每个进程都有父进程,由init进程调用wait函数进行善后 

6.2 wait函数

功能:获取任意子进程的状态改变信息(如果是终止状态则对子进程进行善后处理) 函数原型

  • 头文件:sys/wait.h
  • pid_t wait(int *statloc);

参数与返回值

  • statloc:用于获取子进程的状态改变
  • 返回值:若成功返回状态信息改变子进程ID,出错返回-1

参数statloc

  • statloc可以为空指针,此时父进程不需要具体了解子进程的状态变化,只是为了防止子进程成为僵尸进程,或者因为同步原因需等待子进程终止
  • 若statloc不是空指针,则内核将子进程状态改变信息存放在它指向的存储空间中

子进程状态改变信息包含了多种类型的信息,可以通过系统提供的宏来快速解析子进程的状态调用wait函数之后,父进程可能出现的情况

  • 如果所有子进程都还在运行,则父进程被阻塞(TASK_INTERRUPTIBLE状态),直到有一个子进程终止或暂停,wait函数才返回
  • 如果已经有子进程进入终止或暂停状态,则wait函数会立即返回
  • 若进程没有任何子进程,则立即出错返回-1

如果一个进程有几个子进程,那么只要有一个子进程状态改变,wait函数就返回 如何才能使用wait函数等待某个特定子进程的状态改变?

  • 调用wait,然后将其返回的进程ID和所期望的子进程ID进行比较
  • 如果ID不一致,则保存该ID,并循环调用wait函数,直到等到所期望的子进程ID为止

6.3 waitpid函数

功能:等待某个特定子进程状态改变 函数原型

  • 头文件:sys/wait.h
  • pid_t waitpid(pid_t pid, int *statloc, int options);

返回值

  • 成功返回终止子进程ID,失败返回-1

参数 ⚫pid

  • pid == -1:等待任意子进程执行终止(同wait)
  • pid > 0:等待进程ID为pid的子进程执行终止
  • pid == 0:等待其组ID等于调用进程组ID的任意子进程
  • pid < -1:等待其组ID等于pid绝对值的任意子进程

⚫statloc:存放子进程终止状态⚫options:可以为0,也可以是以下常量或常量的或

  • WCONTINUED:如果有暂停的进程由于SIGCONT信号的到来而继续运行,则函数将返回
  • WUNTRACED:如果有处于终止状态的进程,则函数返回
  • WNOHANG:如果没有任何已经终止的子进程则马上返回, 函数不等待,此时返回值为0

waitpid的特有功能 ◼waitpid可等待一个特定的进程的状态改变信息 ◼waitpid可以实现非阻塞的等待操作,有时希望取得子进程的状态改变信息,但不希望阻塞等待子进程状态改变◼waitpid支持作业控制(进程组控制 )  

7 在进程中运行可执行文件

7.1 exec系列函数

◼进程调用exec系列函数在进程中加载执行另外一个可执行文件◼exec系列函数替换了当前进程(执行该函数的进程)的正文段、数据段、堆和栈(来源于加载的可执行文件)◼执行exec系列函数后从加载可执行文件的main函数开始重新执行◼exec系列函数并不创建新进程,所以在调用exec系列函数后其进程ID并未改变,已经打开的文件描述符不变◼execl execle execlp execv execve execvp◼六个函数开头均为exec,所以称为exec系列函数

  • l:表示list,每个命令行参数都说明为一个单独的参数
  • v:表示vector,命令行参数放在数组中
  • e:表示由函数调用者提供环境变量表
  • p:表示通过环境变量PATH来指定路径,查找可执行文件

7.2 execl函数

函数原型

  • 头文件:unistd.h
  • int execl(const char *pathname,const char *arg0, …,NULL);

参数

  • pathname:要执行程序的绝对路径名
  • 可变参数:要执行程序的命令行参数,以空指针结束

返回值

  • 出错返回-1
  • 成功该函数不返回!

7.3 execv函数

函数原型

  • 头文件:unistd.h
  • int execv(const char *pathname, char *const argv[]);

参数

  • pathname:要执行程序的绝对路径名
  • argv:数组指针维护的程序命令行参数列表,该数组的最后一个成员必须为空指针

返回值

  • 出错返回-1
  • 成功该函数不返回

7.4 execle函数

函数原型

  • 头文件:unistd.h
  • int execle(const char *pathname, const char *arg0,… NULL, char *const envp[]);

参数

  • pathname:要执行程序的绝对路径名
  • 可变参数:要执行程序的命令行参数,以空指针结束
  • envp指向环境字符串指针数组的指针,该数组的最后一个成员必须为空指针

返回值

  • 出错返回-1
  • 成功该函数不返回

7.5 其他exec函数

 

execve函数

  • int execve(const char *pathname,char *const argv[], char *const envp[]);

execlp函数

  • int execlp(const char *filename,const char *arg0, …,NULL);
  • filename参数可以是相对路径(路径信息从环境变量PATH中获取)
  • 例如默认环境变量中包含的PATH=/bin:/usr/bin:/usr/local/bin/

execvp函数

  • int execvp(const char *filename,char *const argv[]);

7.6 exec类函数之间的关系

8 Linux线程控制

8.1 线程的基本概念

 ◼ 进程的概念体现出两个特点:资源(代码和数据空间、打开的文件等)以及调度/执行。线程是进程内的独立执行代码的实体和调度单元◼ 一个进程内的所有线程共享进程的很多资源(这种共享又带来了同步问题)  

8.2 线程与进程的对比

线程只拥有少量在运行中必不可少的资源

  • PC指针:标识当前线程执行的位置
  • 寄存器:当前线程执行的上下文环境
  • 栈:用于实现函数调用、局部变量(局部变量是私有的)

进程占用资源多,线程占用资源少,使用灵活 线程不能脱离进程而存在,线程的层次关系,执行顺序并不明显,会增加程序的复杂度 没有通过代码显示创建线程的进程,可以看成是只有一个线程的进程  

8.3 线程ID

 ◼同进程一样,每个线程也有一个线程ID◼进程ID在整个系统中是唯一的,线程ID只在它所属的进程环境中唯一◼线程ID的类型是pthread_t,在Linux中的定义:  

  • typedef unsigned long int pthread_t( /usr/include/bits/pthreadtypes.h)

pthread_self函数可以让调用线程获取自己的线ID 函数原型

  • 头文件:pthread.h
  • pthread_t pthread_self();

返回调用线程的线程ID

8.4 线程的创建

pthread_create函数用于创建一个线程 函数原型

  • 头文件:pthread.h
  • int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);

调用pthread_create函数的线程是所创建线程的父线程 参数

  • tidp:指向线程ID的指针,当函数成功返回时将存储所创建的子线程ID
  • attr:用于指定线程属性(一般直接传入空指针NULL,采用默认线程属性)
  • start_rtn:线程的启动例程函数指针,创建的线程首先执行该函数代码(可以调用其他函数)
  • arg:向线程的启动例程函数传递信息的参数

返回值

  • 成功返回0,出错时返回各种错误码

8.5 线程的终止

线程的三种终止方式

  • 线程从启动例程函数中返回,函数返回值作为线程的退出码
  • 线程被同一进程中的其他线程取消
  • 线程在任意函数中调用pthread_exit函数终止执行

 线程终止函数◼函数原型

  • 头文件:pthread.h
  • void pthread_exit(void *rval_ptr);

◼参数

  • rval_ptr:该指针将参数传递给pthread_join函数(与exit函数参数用法类似)

8.6 取消线程

线程调用该函数可以取消同一进程中的其他线程(即让该线程终止) 函数原型

  • 头文件: pthread.h
  • int pthread_cancel(pthread_t tid);

参数与返回值

  • tid:需要取消的线程ID
  • 成功返回0,出错返回错误编号

◼在默认情况下,pthread_cancel函数与被取消线程(ID等于tid的线程)自身调用pthread_exit函数(参数为PTHREAD_CANCELED)效果等同◼线程可以选择忽略取消方式或者控制取消方式◼pthread_cancel并不等待线程终止,它仅仅是提出请求

8.7 父线程等待子线程终止

函数原型

  • 头文件:pthread.h
  • int pthread_join(pthread_t thread,void **rval_ptr);

调用该函数的父线程将一直被阻塞,直到指定的子线程终止 返回值

  • 成功返回0,否则返回错误编号

8.7.1 pthread_join函数

参数

  • thread:需要等待的子线程ID
  • rval_ptr:(若不关心线程返回值,可直接将该参数设置为空指针)

            1.若线程从启动例程返回,rval_ptr将包含返回码            2.若线程被取消,rval_ptr指向的内存单元值置为PTHREAD_CANCELED            3.若线程通过调用pthread_exit函数终止,rval_ptr就是调用pthread_exit时传入的参数

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » Linux学习-3进程与线程