进程和线程

多进程与多线程:进程是系统资源分配的最小单位,线程是程序执行的最小单位,即CPU调度的基本单位。

  • 进程是执行中的目标码( executable object code),正在运行中的程序。进程是由数据、资源、状态及虚拟的一个计算机 组成的。
    进程和线程_第1张图片

  • 可执行格式包括:元数据(meta data)及多个代码和数据段(sections of code and data)。这些段是会被加载进内存的线性块的目标码( linear chunk)。

    • 最重要和最常用的段是text段(也就是code段)、 data段和bss段。
      text段 包含可执行的代码及只读数据(const),通常被标识成只读或可执行
      data段 包含已经初始化的数据(例如定义过初始值的C 变量 ),通常被标识成可檫写
      bss段 包含尚未初始化的全局变量,内核会将此段映射成zero page (填满零的页面)。
  • 进程还会涉及由内核仲裁和管理的各种系统资源,进程通常只能通过系统调用来请求和操作资源(包括定时器、未决信号、已打开的文件、网络连接、硬件以及IPC机制)。

  • 在内核中,一个进程所配的资源会随着进程相关的数据及统计值,一同被存入进程中的进程描述符(process descriptor)。

    • 进程是一个虚拟的抽象对象,内核支持抢占式多任务处理(preemptive multitasking)和虚拟内存(virtual memory),并为进程提供虚拟化的处理器及内存的虚拟化视图。多个进程可能同时参加调度,但在运行时,每个进程都好像在独自掌控整个系统一样。事实上:内核会无间隙且透明的抢占和重新调用进程(preempts and reschedule process),与正在运行的所有进程共享处理器。因为每个进程被分配单一的线性地址空间,给出每个进程在独自控制系统中所有内存的假象。举例:电表程序可以看成是一个进程。
    • 内核通过现代处理器所提供的硬件支持,来管理虚拟内存和页面调度(paging)机制,让每个进程各自运行在不同的地址空间中,允许多个进程并存在系统中。
  • 进程与程序:

    • 程序是包含可执行代码及执行代码需要的数据等信息的文件,存放在磁盘等介质上。
    • 当程序被操作系统装载到内存并分配给它一定的资源后,此时可以称为进程。
    • 程序是静态概念,进程是动态概念。
  • 进程在内核中的组织形式:进程控制块
    进程和线程_第2张图片

  • linux进程控制块:task_struct结构
    pid_t pid; struct list_head children,sibling;
    uid_t uid,euid; struct fs_struct *fs;
    gid_t gid,egid; struct files_struct *files;
    volatile long state; struct mm_struct *mm;
    int exit_state; struct signal_struct *signal;
    unsigned int rt_priority; struct sighand_struct *sighand;
    unsigned int policy; cputime_t utime,stime;
    struct list_head tasks; struct timespec start_time;
    struct task_struct *real_parent; struct timespec real_start_time;
    struct task_struct *parent;

    • task_struct :进程状态:
      进程状态 state成员的可能取值如下
      volatile long state; #define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define TASK_ZOMBIE 4 #define TASK_STOPPED 8
      • 进程状态切换:
        进程和线程_第3张图片
  • 进程属性

    • 进程基本属性—进程ID
      • 获取进程ID
        头文件/usr/include/unistd.h
        pid_t getpid(void);//pid_t 实际上就是int类型;执行成功时返回当前进程ID;失败返回-1(错误原因存储在errorno中)。
      • 进程属性 : 与进程相关联的用户ID包含以下类型
        进程真实用户和用户组 我们实际是谁(即执行程序或创建子进程的用户)
        进程有效用户和用户组 用于文件访问权限检查(用户ID是进程的重要属性)
      • 真实用户与有效用户的关系:
        • 通常情况下,有效用户和真实用户相同(有效用户ID等于真实用户ID)。
        • 进程真实、有效用户和用户组总结如下表格
          进程真实用户和用户组 进程有效用户和用户组
          概念:进程真实用户为执行命令/可执行文件的用户,真实用户组是真实用户所在的组 进程有效用户和有效用户组只有当执行文件设置了setuid位或setgid位时才会发生变化
          获取进程真实用户ID: 头文件:/usr/include/unistd.h ;函数:uid_t getuid(void);//执行成功返回当前进程的真实用户ID,失败返回-1(错误你存储在errorno中) 获取进程有效用户ID: 头文件:/usr/include/unistd.h ;函数:uid_t geteuid(void);//执行成功返回当前进程的有效用户ID,失败返回-1(错误你存储在errorno中)
          获取进程真实用户组ID:头文件:/usr/include/unistd.h ;函数:uid_t getgid(void);//执行成功返回当前进程的真实用户组ID,失败返回-1(错误你存储在errorno中) 获取进程有效用户组ID:头文件:/usr/include/unistd.h ;函数:uid_t getegid(void);//执行成功返回当前进程的有效用户组ID,失败返回-1(错误你存储在errorno中)
    • 有效用户和真实用户的suid代码示例:
      #include
      #include
      #include
      #include
      int main(void)
      {
      printf(“real uid:%d,real gid:%d\n”,getuid(),getgid());
      printf(“effective uid:%d,effective gid:%d\n”,geteuid,getegid());
      }
  • 进程声明周期:

    • 进程的创建:
    • 进程的启动:
      • C程序的启动函数是main,也就是进程代码的入口点
        • main(int argc, char *argv[]);
      • 当内核启动C程序时,会在main函数前调用特殊的启动函数来获取main函数地址和传递给main函数的参数,并且将这些信息填写到进程控制块中。
    • 进程的终止:
      • 正常终止:
        • 从main()函数中返回
        • 在任意代码中调用exit函数或_exit函数
        • 最后一个线程从其启动例程中返回
        • 最后一个线程调用pthread_exit函数
      • 异常终止:
        • 在任意代码中调用abort函数
        • 接收到终止信号。
    • 进程的生命周期
      • 这里欠一张图
    • 终止进程的函数
      • 头文件stdlib.h,函数定义:void exit(int status)
      • 头文件unistd.h,函数定义:void _exit(int status)
      • 调用这两个函数均会正常的终止一个进程
      • 调用_exit函数俊辉立即返回内核
      • 调用exit函数:
        • 执行预先注册的终止处理函数
        • 执行文件I/O操作的善后工作,使得所有缓冲的输出数据被更新倒相应的设备
        • 返回内核
    • exit和return的区别:
      • return是C语言关键字,exit是POSIX的API函数
      • 在main函数中,执行return和调用exit函数会产生相同的效果
      • 在子函数中,执行return仅仅从子函数中返回,而调用exit函数将会退出当前进程。
    • 注册终止处理函数:
      • 当进程终止时,程序可能需要进程一些自身的清理工作:如日志登记、资源释放等
      • 通过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示例代码:
      void fun1()
      {
      printf(“func1 is called\n”);
      }
      void fun2()
      {
      printf(“func2 is called\n”);
      }
      void fun3()
      {
      printf(“func3 is called\n”);
      }
      int main(int argc,char **argv)
      {
      atexit(func1);
      atexit(func2);
      atexit(func3);
      printf(“process exit\n”);
      return 0;
      }
    • on_exit示例代码
      void func1(int status,void arg)
      {
      printf(“func1 exit status is %d\n”,status);
      printf(“func1 arg is %d\n”,
      ((int*)arg));
      }
      void func2(int status,void arg)
      {
      printf(“func2 exit status is %d\n”,status);
      printf(“func2 arg is %d\n”,
      ((int*)arg));
      }
      void func3(int status,void arg)
      {
      printf(“func3 exit status is %d\n”,status);
      printf(“func3 arg is %d\n”,
      ((int*)arg));
      }
      int i,j,k;
      int main(int argc,char argv)
      {
      i = 3;
      on_exit(fun1,(void
      )&i);
      j = 4;
      on_exit(func2,(void
      )&j);
      k = 5;
      on_exit(func3,(void*)&k);
      printf(“process exit\n”);
      return 0;
      }
  • 进程环境:

    • 进程内存空间布局
      这里欠张图
    • C程序内存空间布局
      这里欠张图
    • 环境变量表
      • 每个进程都会有自己的环境变量表
      • 通过全局的环境指针(environ)可以直接访问环境变量表(字符串数组)
      • 头文件unistd.h extern char **environ;
      • 环境变量字符串形式位“name = value",name是环境变量名称,value是环境变量复制
        这里欠张图
    • 获取环境变量
      • 获取环境变量的方法:
        • 直接通过environ变量访问环境表
        • 使用getenv函数,获取环境变量值
          头文件:stdlib.h
          char *getenv(const char *name);//指定环境变量名称,返回环境变量字符串指针,若未找到则返回空指针。
    • 设置环境变量:
      • 设置环境变量的3钟方法:putenv、setenv、unsetenv。
        函数名 功能
        int putenv(char str); 将环境变量字符串放入到环境变量表中吗;若该字符串已经存在,则覆盖。
        int setenv(const char
        name,const char *value,int rewite) 将制定环境变量的值设置为参数指定值(更改环境变量字符串)。若name已经存在,rewrite不等于0,则删除其原先的定义;若rewrite等于0,则不用删除原先的定义。
        int unsetenv(const char *name); 删除指定的环境变量字符串
  • 创建进程:

    • linux中创建进程的方式:
      • 在shell中执行命令或可执行文件:在shell进程中调用fork函数创建子进程
      • 在代码中(已经创建的进程中),调用fork函数创建子进程。
    • linux系统中进程0(PID = 0)是有内核创建,其他所有进程都是有父进程调用fork函数创建的。
    • linux系统中的进程0在创建子进程(PID =1,init进程)后,进程0就转为交换进程或空间进程。进程1(init进程)是系统中其他所有进程的共同祖先。
    • 创建子进程:
      • 函数 功能
        pid_t fork(void); fork函数被正确调用后,将会在子进程和父进程中分别返回。在子进程中返回值为0(不合法的PID,提示当前运行在子进程中);父进程中的返回值为子进程ID(让父进程掌握所创建子进程的ID号);出错返回-1.
  • 创建子进程实例:
    int main(int argc,char **argv)
    {
    pid_t pid;
    pid = fork();
    if(pid == -1)
    printf(“fork error\n”);
    else if (pid == 0)
    {
    printf(“the return value is %d\n”,pid);
    printf(“in child process!!\n”);
    printf(“My pid is %d\n”,getpid());
    }
    else
    {
    printf(“the returned value is %d\n”,pid);
    printf(“in father process!!\n”);
    printf(“my id is %d\n”,getpid());
    }
    return 0;
    }

  • 进程内存空间分布:
    欠一张图

    • fork函数工作流程:
      • 子进程是父进程的副本
        • 子进程复制/拷贝父进程的PCB、数据空间(数据段、堆和栈)。
        • 父子进程共享正文段(只读)
      • 子进程和父进程继续执行fork函数调用之后的代码。
      • 为了提高效率,fork后并不立即复制父进程数据段、堆和栈,采用了写时的复制机制(copy - on_write)
      • 当父子进程任意之一要修改数据段、堆和栈时,进程复制操作,并且仅复制修改区域。
      • fork函数执行后父子进程的主要异同,如下表格
        父子进程相同点 父子进程不同点
        真是用户ID,真实组ID fork的返回值
        有效用户ID,有效组ID 进程ID和父进程ID
        环境变量 子进程的tms_utime;tms_stime;tms_cutime;tms_ustime值被设置为0


        打开的文件
    • 父子进程共享文件
      这里欠张图
    • 父子进程对共享文件的常见处理方式
      • 父进程等待子进程完成。当子进程终止后,文件当前位置已经得到相应的更新
      • 父子进程各自执行不同的程序段,各自关闭不需要的文本
    • fork函数的用法:
      • 父进程希望父子自己(共享代码,复制数据空间),但父子进程执行相同代码中的不同分支。
      • 网络服务程序中,父进程等待客户端的服务请求,当请求达到时,父进程调用fork创建子进程处理该请求,而父进程继续等待下一个服务请求到达。
      • 父子进程执行不同的可执行文件(父子进程具有不同的代码段和数据空间)。
        • 子程序从fork函数返回后,会立即调用exec类函数执行另外一个可执行文件。
    • vfork函数:
      • vfork用于创建新的进程,而该新进程的目的是执行另外一个可执行文件
      • 由于新程序将有自己的地址空间,因此vfork函数并不将父进程的地址空间完全复制到子进程中
      • 子进程在调用exec或exit之前,在父进程的地址空间中进程
      • vfork函数保证子进程先执行,在它调用exec或exit之后,父进程才会继续被调用执行。父进程处于task_uninterruptible。
  • 获知子进程运行状态改变

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

    • 进程在退出之前会释放进程用户空间的所有资源,但PCB等内核空间资源不会释放。
      • 当父进程调用wait或waitpid函数后,内核将根据情况关闭该进程打开的所有文件,释放PCB(释放内核空间资源)。
      • 对于已经终止但父进程尚未对其调用wait或waitpid函数的进程(TASK- ZOMBIE状态),称为僵尸进程。
  • 父子进程的关系:

  • 父子进程的关系:。关于资源,子进程除了代码段和父进程共享以外,其他的就是一个复制过来的副本,二者并不共享地址空间,两个是单独的进程,之后就没太多关系了,子进程单独运行。

    • 关于文件描述符,父子进程共享文件表项,一个进程修改,另一个进程也知道此文件被修改了。
  • 线程:

  • 线程与进程之间的关系:

    • 一个进程的所有线程都共享该进## 标题程获得的资源。
    • 各线程有属于自己的一小部分资源,就是栈空间,保存其运行状态和局部自动变量的。
      线程中使用malloc等申请的空间都是占的进程的资源,即堆资源。

多进程通信方式: 1.管道(无名管道,有名管道);2.信号;3.共享内存; 4.消息队列

  • 管道:
    • 1,管道是半双工的,管道允许进程间按先进先出的方式传送数据
    • 2,无名管道只可在有亲缘关系的进程之间通信,有名管道则都可以
    • 3,管道的实质其实是一个内核缓存区,该缓存区可看作是一个循环队列
  • 信号:
    • 1.信号是Linux系统中用于进程之间通信或操作的一种机制,信号可以在任何时候发送给某一进程
    • 2.信号是软件层次上对中断机制的一种模拟,是一种异步通信方式
    • 3.信号事件来源,硬件来源,软件来源
    • 4.SIGKILL、SEGSTOP应用进程无法捕捉和忽略,这是为了使系统管理员能在任何时候中断或结束某一特定的进程。
  • 共享内存:
    • 1.共享内存即允许两个或多个进程共享一个存储区,这段存储区进程可以映射至自身地址,大家共享这块存储区域
    • 2.共享内存执行效率高,可直接读写内存不需任何数据拷贝。
  • 消息队列:
    • 1,消息队列就是一个消息链表,是一系列保存在内核中消息的列表
    • 2,消息队列对于管道通信的优点就是可以按需要读取特定类型消息,不需要按队列次序
  • 多线程同步:
    • 两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据
  • 多线程互斥:
    • 一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
  • 多进程与多线程的区别及优劣:
    • 1.进程创建开销大,线程创建开销小
    • 2.进程间通信比较麻烦,线程间通信比较方便
    • 3.多进程程序更加健壮,多线程出现某个线程挂掉导致整个业务挂掉,多进程有独立的地址空间,一个进程挂掉影响可能并不大。
  • IO多路复用:select和poll及epoll
    • select和poll基本是差不多的,除了select有文件描述符限制(1024个),其他和poll没有太大区别。每次调用都需要将fd集合拷贝到内核态且遍历所有的fd,开销很大。
    • epoll在注册新事件时就会把所有fd拷贝到内核,只需拷贝一次即可,epoll是只关心活跃的fd,通过回调机制提高效率。

你可能感兴趣的:(linux操作系统)