Skip to content

Latest commit

 

History

History
1570 lines (1135 loc) · 45.7 KB

File metadata and controls

1570 lines (1135 loc) · 45.7 KB

进程

进程环境

main函数和命令行参数

通常,C程序总是从main函数开始执行

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

argc:命令行参数的个数

argv:指向命令行参数的各个指针所构成的数组,指向环境变量表

进程的启动和终止

8种方式使进程终止

正常终止

  • 从main返回
  • 调用exit (并不好,终止了回收)
  • 调用 _exit或 _Exit
  • 最后一个线程从其启动例程返回
  • 最后一个线程调用pthread_exit

异常终止

  • 调用abort
  • 接到一个信号并终止
  • 最后一个线程对取消请求做出响应

进程终止

void exit(int status);
void _Exit(int status);
void _exit(int status);

status:进程的终止状态

exit函数执行一个标准I/O库的清理关闭操作(为所有打开流调用fclose函数)后,进入内核

_Exit、 _exit函数立即进入内核

#include<stdlib.h>
int main(){
  exit(34);
}//echo $?查看终止状态

atexit函数

当进程终止时,程序可能需要进行一些自身的清理工作,如资源释放等等

atexit函数提供了进行这样工作的机会

它允许用户注册若干终止处理函数,当进程终止时,这些终止处理函数将会被自动调用

int atexit(void (*func)(void));

func:函数指针,返回值为void,无参

成功返回0,出错返回非0值

#include <iostream>
#include <stdlib.h>

using namespace std;

void f(){
  cout << "in function f()" << endl;
}
void g(){
  cout << "in function g()" << endl;
}
void h(){
  cout << "in function h()" << endl;
}

int main(){
  atexit(f);
  atexit(g);
  atexit(h);
  exit(4);
}
$./a.out
in function h()
in function g()
in function f()

可见任务是以栈顺序注册,退出也是按栈的方式退出

现在 //exit(4); 虽然我们没有写return,编译器也会自动添加,从栈上取回返回地址返回C库

再将 exit(4)换成_exit(4);因为立即进入内核,C库无法进行调用

注意:先注册的函数,后被运行

调用_exit函数并不会触发终止处理函数

进程的启动和终止

环境表和环境变量

每个进程都会接收到一张环境表,通过environ找到环境表

extern char **environ;

环境字符串:name=value

环境表的结构

C 数组传入函数方法

  1. 数组,长度
  2. 数组头指针,数组尾指针
  3. 给头指针(结尾放NULL)

访问环境变量的方法

  • 直接使用environ
  • 使用getenv和putenv等函数

getenv函数用于获取环境变量值

char* getenv(const char *name);
  • 返回与name关联的value的指针(等号后面的内容),若未找到则返回NULL
  • 返回的指针是指向新分配的内存,还是环境表中存在的值?(程序4.4)
int main(void){
    char *p = getenv("SHELL");
    cout << p << endl;
    cout << "value of p: " << std::hex << (unsigned long)p << endl;
    cout << "value of environ: " << std::hex << (unsigned long)environ << endl;

    char **q = environ;
    for(;;){
        if(*q == NULL) break;
        cout << std::hex << (unsigned long)(*q) << " : " << *q << endl;
        q++;
    }
    return 0;
}
p: 7ffce5940f0d #这是等号后第一个字符的位置,所以返回表上的地址
7ffce5940f07 : SHELL = /bin/bash

返回的指针是在表内指向的地方,并不是新分配的地方

设置环境变量

三种方法:

  • putenv
  • setenv
  • unsetenv

putenv函数将形式为name=value的字符串,放入环境表中;若name已经存在,则先删除其原来的定义。

int putenv(char *str);
int setenv(const char* name, 
             const char* value, 
             int rewrite);
  • setenv将环境变量name的值设置为value。
  • 若name已经存在
    • rewrite != 0,则删除其原先的定义
    • rewrite == 0,则不删除其原先的定义

nunsetenv函数用于删除某个环境变量

int unsetenv(const char* name);

删除name的定义

char *item1 = "Student=OK";
int main(){
    char *item2 = new char[10];
    item2[0] = 'A';
    item2[1] = '=';
    item2[2] = 'B';
    item2[3] = 0;

    char item3[4];	//在栈上,是一个局部数组
    item3[0] = 'C';
    item3[1] = '=';
    item3[2] = 'D';
    item3[3] = 0;

    //直接使用内存,没有进行新的分配空间,直接将环境表指针指向这个变量
    putenv(item1);
    putenv(item2);
    putenv(item3);

    char **q = environ;
    for (;;){
        if (*q == NULL) break;
        cout << std::hex << (unsigned long)(*q) << " : " << *q << endl;
        q++;
    }

    cout << "address of item1: " << hex << (unsigned long)item1 << endl;
    cout << "address of item2: " << hex << (unsigned long)item2 << endl;
    cout << "address of item3: " << hex << (unsigned long)item3 << endl;
    //如果打印结果内容相同,则说明环境表直接使用程序传入的参数
    return 0;
}

存储空间布局

C程序存储空间布局

C程序存储空间布局

7FF...最高的用户态地址

  • 栈: 主要用于支撑函数调用存放参数、局部变量等

  • 堆:用于动态分配内存

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

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

  • 正文:CPU执行的机器指令部分,正文段通常是共享、只读的

不同系统的进程地址空间

Win95到2000 共享空间少,系统卡死情况就变少了

Linux64位的空间分配比32位简单,因为资源宽松,没有复杂的手段

查看Linux进程的地址空间

  • $cat /proc/进程ID/maps
  • /proc目录中的文件并不是真正的磁盘文件,而是由内核虚拟出来的文件系统,当前系统中运行的每个进程在/proc下都有一个子目录,目录名就是进程的id,查看目录下的文件可以得到该进程的相关信息。

存储器分配

内存分配

三个用于内存分配的函数

  • malloc:分配指定字节数的存储区,此存储区中的初始值不确定
  • calloc:为指定数量指定长度的对象分配存储空间,该空间中的每一位都初始化为0
  • realloc:更改(增加或者减少)以前分配区的长度

C++中使用new

void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);

注意:三个函数返回的指针一定是适当对齐的

如:在一个特定的系统上,如果最严格的对齐要求是,double必须在8的倍数地址单元处开始,那么这三个函数返回的指针部分都应这样对齐

内存释放

free函数用于释放已分配的内存,即将内存归还给堆

void free(void *ptr);

分配内存想释放不能释放才是内存泄漏,分配不释放不属于内存泄漏

setjmp和longjmp函数

void g(){}
void f(){
  g();
}
void main{
    f();
}

在左侧的代码中,main函数调用了f函数,f函数中又调用了g函数

假设当g函数内部处理出错时,希望main函数能够感知到这一出错情况

这种情况下,最好重构代码:star2:

g函数内部处理出错,main函数能被通知到,可能的方法:

  • 用goto,不行一般在函数内部跳转,但层级一多也麻烦

  • g函数通过返回值通知f函数,f函数再通过返回值通知main函数?(考虑函数调用深度)

  • 使用setjmp和longjmp

  • 使用C++异常处理

setjmp和longjmp函数实现函数之间的跳转

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

setjmp函数用于设置跳转的目的位置

longjmp函数进行跳转

  • env:保留了需要返回的位置的堆栈情况
  • setjmp的返回值:直接调用该函数,则返回0;若由longjmp的调用,导致setjmp被调用,则返回val(longjmp的第二个参数)

各类变量的情况

#include<setjmp.h>
jmp_buf jmpbuffer;

void g(){
  cout << "in g()" << endl;
  longjmp(jmpbuffer, 2);	//跳回标签处,setjump返回值为2
}

void f(){
  cout << "in f()" << endl;
  g();
  cout << "leave f()" << endl;
}

int globval;
int main()
{
  int autoval;
  register int regival; // 寄存器变量
  volatile int volaval; // 易失性内存
  static int statval;  

  cout << "begin" << endl;

  globval = 90;
  autoval = 91;
  regival = 92;
  volaval = 93;
  statval = 94;
  
  int i = setjmp(jmpbuffer);	//打标签,当前现场存储到jmpbuffer中
  cout << "setjmp return code: " << i << endl; //返回信息应该是0
  if(2 == i)	//运行g后进入if中
  {
    cout << "error code: "<< i << endl;
    cout << "globval= " << globval << "; "; // 0
    cout << "autoval= " << autoval << "; ";
    cout << "regival= " << regival << "; "; // 92
    cout << "volaval= " << volaval << "; "; // 3
    cout << "statval= " << statval << "; " << endl; // 4
    return 0;
  }

  globval = 0;
  autoval = 1;
  regival = 2;
  volaval = 3;
  statval = 4;

  cout << "globval= " << globval << "; ";
  cout << "autoval= " << autoval << "; ";
  cout << "regival= " << regival << "; ";
  cout << "volaval= " << volaval << "; ";
  cout << "statval= " << statval << "; " << endl;

  f();  
  return 0;
}
#不进行优化
90 91 92 93 94
setjmp return code: 0
0 1 2 3 4
in f()
in g()
setjmp return code: 2
error code: 2
0 1 92 3 4	#寄存器变量保存在jmp_buf中,会被还原

#进行优化
$ g++ test.cpp -O
...
0 91 92 3 4 #在栈上的局部变量,被优化的自动变量放到了寄存器里面

当调用longjmp函数后,在main中的各类变量的值是否改变回原来的值?

  1. 全局变量、静态变量、易失变量不受优化的影响
  2. 在优化的版本,自动变量和寄存器变量存储在寄存器中

变量回滚

typedef struct{
    unsigned j_sp;  // 堆栈指针寄存器
    unsigned j_ss;  // 堆栈段
    unsigned j_flag;  // 标志寄存器
    unsigned j_cs;  // 代码段
    unsigned j_ip;  // 指令指针寄存器
    unsigned j_bp; // 基址指针 
    unsigned j_di;  // 目的指针
    unsigned j_es; // 附加段
    unsigned j_si;  // 源变址
    unsigned j_ds; // 数据段
} jmp_buf; //老版本的结构体
  • 保证局部变量在longjmp过程中一直保存它的值的方法:把它声明为volatile变量。(适合那些在setjmp执行和longjmp返回之间会改变的变量)
  • 存放在内存中的变量,将具有调用longjmp时的值,而在CPU和浮点寄存器中的变量则恢复为调用setjmp函数时的值
  • 优化编译时,register和auto变量都存放在寄存器中,而volatile变量仍存放在内存

volatile变量:一般在多线程中使用的比较多

  • 例如有一个int x,有两个线程都要对其读写
  • 有些编译器或CPU会将x保存在寄存器中,读的时候直接读取寄存器中的内容,而不是真实的x在内存中的内容
  • 线程1,对x进行加1操作,此时内存中x的值为2
  • 线程2想读x,结果从寄存器中读出1
  • 给变量加上volatile,指示程序每次读写变量都必须从内存中读取,不要进行缓存(寄存器)

自动变量潜在问题

#include        <stdio.h>
#define DATAFILE        "datafile"
FILE * open_data(void)
{
    FILE    *fp;
    char    databuf[BUFSIZ];  /* setvbuf makes this the stdio buffer */
    //这里的databuf是放在栈上面的,是一个局部的数组
    
    if ( (fp = fopen(DATAFILE, "r")) == NULL)
        return(NULL);
    // databuf是个局部变量,在退出前。C库认为是空的所以用了这个databuf,导致了databuf可能被覆盖或占用
    if (setvbuf(fp, databuf, BUFSIZ, _IOLBF) != 0) 
        return(NULL);
    return(fp);             /* error */
}

问题:

  • open_data函数返回后,它在栈上所使用的空间将由下一个被调用函数所占用
  • 但是标准I/O库仍使用位于栈上的databuf缓冲区
  • 存在冲突和混乱

解决办法:

  • 使用全局存储空间
  • 使用静态存储空间
  • 从堆中分配
/* 异常处理 */
 void g(){
  printf("in g()\n");
  throw(32); // 逐层抛异常
}
void f(){
  printf("in f()\n");
  g();
  printf("leave f()\n");
}

int main(){
  printf("begin\n");
  
  try{ // C语言没有try和catch
      f();
  }
  catch(int a){
      printf("in catch: %d\n", a);
  }  

  return 0;
}
// head.h // try和catch的实现藏在这里
#include<setjmp.h>
jmp_buf jmpbuffer;
#define try \
    int jmp; \
    jmp = setjmp(jmpbuffer); \
    if(0 == jmp) \

#define throw(a) longjmp(jmpbuffer, a);

#define catch(a) \
    a = jmp; \
    if(0 != jmp)
$./a.out
begin
in f()
in g()
in catch: 32

Resource limits 资源限制

Every process has a set of resource limits, some of which can be quire and changed by following functions.

#include  <sys/time.h>
#include  <sys/resource.h>
int  getrlimit ( int resource,  struct  rlimit  *rlptr );
int  setrlimit ( int resource,  const struct  rlimit  *rlptr );
 
struct  rlimit {
	rlim_t     rlim_cur;	/* soft limit:  current limit */
	rlim_t     rlim_max;   	/* hard limit:  maximum value for rlim_cur */
}

resource musta be on of

  • RLIMIT_CPU. CPU time limit in seconds. When the process reaches the soft limit, it is sent a SIGXCPU signal.
  • RLIMIT_DATA 数据段. The maximum size of the process data segment (initialized data, uninitialized data, and heap).
  • RLIMIT_FSIZE 进程文件最大大小. The maximum size of files that the process may create. Attempts to extend a file beyond this limit result in delivery of a SIGXFSZ signal.
  • RLIMIT_LOCKS. A limit on the combined number of flock() locks and fcntl() leases that this process may establish.
  • RLIMIT_MEMLOCK. The maximum number of bytes of virtual memory that may be locked into RAM using mlock() and mlockall().
  • RLIMIT_NOFILE 进程可打开最大文件描述符数量. Specifies a value one greater than the maximum file descriptor number that can be opened by this process.
  • RLIMIT_NPROC. The maximum number of processes that can be created for the real user ID of the calling process.
  • RLIMIT_STACK. The maximum size of the process stack, in bytes. Upon reaching this limit, a SIGSEGV signal is generated.
  • Etc.

Three rules govern the changing of the resource limits:

  1. A soft limit can be changed by any process to a value less than or equal to its hard limit. 软的限制可以修改但小于等于硬限制
  2. Any process can lower its hard limit to a value greater than or equal to its soft limits. 进程可以降低硬限制,但大于等于软的限制
  3. Only superuser process can raise a hard limit. 只有超级用户进程可以升高硬限制

进程控制

进程标识符

每个进程都有一个非负整型表示的唯一进程ID

进程ID总是唯一的

当进程终止后,其ID值可以重用

在unix中

  • ID为0的进程:调度进程,称为swapper
  • ID为1的进程:init进程,自举过程结束时由内核调用
  • ID为2的进程:页守护进程,负责支持虚拟存储系统的分页操作

获取进程常见标识符

  • 调用进程的进程ID:pid_t getpid();
  • 调用进程的父进程ID:pid_t getppid();
  • 调用进程的实际用户ID:uid_t getuid();
  • 调用进程的有效用户ID:uid_t geteuid();
  • 调用进程的实际组ID:gid_t getgid();
  • 调用进程的有效组ID:gid_t getegid();

查看进程情况

$ps -ef | less

fork等函数

一个进程可以调用fork函数创建一个新进程

新进程被称为子进程

pid_t fork(void);
  • fork函数调用一次,但是返回两次
  • 在子进程中返回0,在父进程中返回子进程ID,出错返回-1
  • 通过返回值,可以确定是在父进程还是子进程中

fork父子进程数据结构

父进程 page(线性Addr)=物理地址 指向父进程的正文段和数据段

子进程 父进程 对应了自己的正文段(优化时只读的共享直接指向父进程正文段)和数据段同一个页框

当优化时:子进程正文段指向父进程正文段物理地址

window会指定执行文件,小于等于4字节 eax寄存器返回,子进程ID eax寄存器


子进程和父进程继续执行fork调用之后的指令(父进程通过fork返回值得知子进程ID)

子进程是父进程的副本

  • 子进程获得父进程数据空间、堆和栈的副本

  • 父子进程并不共享这些存储空间

  • 父子进程共享正文段(只读的)

为了提高效率,fork后不并立即复制父进程空间,采用了COW(Copy-On-Write)写时复制,所以一开始数据段指向同一个数据段(页框)

一开始页框(pgd,pud,pmd,pt)是只读的,一旦复制 MOV指令,转到PF指令,创建新的页框

  • 当父子进程任意之一,要修改数据段、堆、栈时,进行复制操作,但仅复制修改区域
int glob = 6;
char buf[] = "a write to stdout\n";
//char p[] = "hello,world"; 是对字符串"hello,world"做了拷贝,所以可以对拷贝字符串做任意修改
//char *p = "hello,world"; 字符串常量将自己在静态存储区中的地址赋给了p指针 
int main(void){
        int var;
        pid_t pid;
        var = 88;
        if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1){ //写buf,无缓冲,直接打印
                cout << "write error" << endl;
                return 0;
        }

        printf("before fork\n"); //当把输出到屏幕的东西重定向到文件,行缓冲变全缓冲。如果进行重定向输出到文件,这时printf并没有往文件里面写,还在父进程缓冲里面
    	//子进程也拥有这个缓冲(拷贝得到)
    	//如果没有重定向的时候缓冲之前被刷新了
        if ((pid = fork()) < 0){
                cout << "fork error" << endl;
                return 0;
        }
        else if (pid == 0){ //子进程执行
                glob++;
                var++;
        }
        else{ //父进程执行
                sleep(2);
        }

        printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
    	return 0;
}
$ ./a.out
a write to stdout
before fork
pid = 2233, glob = 7, var = 89	#子进程
pid = 2232, glob = 6, var = 88	#父进程

$ ./a.out > a #运行结果重定向输出到a中
$ vim a #查看a文件内容
a write to stdout
before fork
pid = 2238, glob = 7, var = 89	#子进程
before fork
pid = 2237, glob = 6, var = 88	#父进程

为什么write调用的输出只有一次,而printf调用的"before fork"输出出现了两次?

  • /write调用是不带用户空间缓冲的。在fork之前调用write,其数据直接写到了标准输出上
  • 标准I/O库是带缓冲的,当标准输出连接到终端设备时,它是行缓冲,否则为全缓冲
  • 当printf输出到终端设备时,由于遇到换行符,因此缓冲被刷。子进程的数据空间中无缓冲内容
  • 当重定向到文件时,变为全缓冲。fork后,子进程的数据空间中也有内容。所以输出两次
  • 两个进程退出时刷缓冲,所以输出两次

子进程中,变量的值改变了;而父进程中,变量的值没有改变。原因?

  • 这种复制是一种副本关系,不是共享关系

在使用fork函数时,一定要牢记子进程复制了父进程的地址空间

  • 父进程在fork之前new了一个对象,子进程需要delete它吗?
    • 需要,fork的子进程,从堆里面复制需要进行释放。但是很难释放
  • 父进程在fork之前open的文件,子进程需要close文件描述符吗?
    • 子进程指向父进程的文件表,指向同一个file对象,引用计数加一,需要close减一引用计数

在上例中:在重定向父进程的标准输出时,子进程的标准输出也被重定向了

fork的一个特性:父进程的所有打开文件描述符,都被复制到子进程中。

父子进程共享文件对象:star:

父子进程共享文件对象

父子进程对同一文件使用了一个文件偏移量

上例中,父进程等待了子进程两秒钟,所以他们的输出才没有混乱;否则有可能出现乱序

文件描述符的常见处理方式

  • 父进程等待子进程完成。父进程无需对描述符做任何处理,当子进程终止后,文件偏移量已经得到了相应的更新
  • 父子进程各自执行不同的程序段,各自关闭文件描述符

Difference between parent and child after fork

Properties inherited from parent 从父进程继承而来

  • Real user/group ID, effective user/group ID
  • Supplementary group ID
  • Process group ID
  • Session ID
  • Control terminal
  • Set-user/group-ID
  • current work directory
  • File mode mask
  • Signal mask
  • environment
  • Resource limits

Difference between parent and child

  • return value from fork
  • Process ID
  • Parent process ID
  • The child’s value for tms_utime ,tms_stime,tms_cutime,tms_ustime are set to 0
  • File locks do not be inherited by child
  • Pending alarm are cleared for child

fork函数常见用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段

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

一个进程要执行一个不同的程序

  • 子进程从fork返回后,立即调用exec执行另外一个程序

代码4.10:封装fork(面向对象,面向API....)(研究生课程)

vfork函数

  • vfork与fork的函数原型相同,但两者的语义不同

  • vfork用于创建新进程,而该新进程的目的是exec一个新程序(执行一个可执行的文件)

  • 由于新程序将有自己的地址空间,因此vfork函数并不将父进程的地址空间完全复制到子进程中。

  • 子进程在调用exec或exit之前,在父进程的地址空间中运行

  • vfork函数保证子进程先执行,在它调用exec或者exit之后,父进程才可能被调度执行

补充:vfork应该尽快调用exec,保证不与除父进程外的其他线程产生冲突

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

using namespace std;

int glob = 6;
int main(void)
{
        int var;
        pid_t pid;
        var = 88;

        printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
        printf("before fork\n");

        if ((pid = vfork()) < 0)
        { // 暂时挂起
                cout << "fork error" << endl;
                return 0;
        }
        else if (pid == 0)
        {
                glob++;
                var++;
                printf("In Child\n");

                _exit(0);
        }

        printf("In Parents\n");
        printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);

        return 0;
}
pid = 2263, glob = 6, var = 88
before fork
In child
In parents
pid = 2263, glob = 7, var = 89 

子进程中对glob、var加1操作,结果改变了父进程中的变量值。原因?父子是一个共享的关系

exit函数

正常终止

  • 从main返回
  • 调用exit:ISO C定义
  • 调用_exit 或 _Exit:前者由ISO C定义,后者由POSIX.1定义
  • 最后一个线程从其启动例程返回
  • 最后一个线程调用pthread_exit

异常终止

  • 调用abort:产生SIGABRT信号
  • 接到某些信号
  • 最后一个线程对取消请求做出响应

不管进程如何终止,最后都会执行内核中的同一段代码:为相应进程关闭所有打开描述符,释放内存等等

父进程在子进程之前终止了,则子进程的父进程将变为init进程,其PID为1;保证每个进程都有父进程

当子进程先终止,父进程如何知道子进程的终止状态(exit(5))

  • 内核为每个终止子进程保存了终止状态等信息
  • 父进程调用wait等函数,可获取该信息

当父进程调用wait等函数后,内核将释放终止进程所使用的所有内存,关闭其打开的所有文件

僵尸进程:star2::对于已经终止、但是其父进程尚未对其调用wait等函数的进程

int  main(void){
	pid_t pid;
	printf("before fork\n"); 

	if ( (pid = fork()) < 0){
		cout << "fork error" << endl;
		return 0;
	}
	else if (pid == 0){
		cout << "Child exit" << endl;
		exit(2);
    } 
	else{
		sleep(600); //子进程已经结束,但是父进程在睡觉没有调用wait函数
	}        
	return 0;
}

程序演示(4.14 后台启动) ps

  • Defunct:死了的 会标记

对于父进程先终止,而被init领养的进程会是僵尸进程吗?

  • init对每个终止的子进程,都会调用wait函数,获取其终止状态

wait等函数

当一个进程正常获知异常终止时,内核就向其父进程发送SIGCHLD信号

父进程可以选择忽略该信号,也可以提供信号处理函数

系统的默认处理方式:忽略该信号,不关心就传个0

wait函数可用于获取子进程的终止状态

pid_t wait(int *statloc);
  • statloc:可用于存放子进程的终止状态
  • 返回值:若成功返回终止进程ID,出错返回-1

调用wait函数之后,进程可能出现的情况

  • 如果所有子进程都还在运行,则阻塞等待,直到有一个子进程终止,wait函数才返回
  • 如果一个子进程已经终止,正等待父进程获取其终止状态,则wait函数会立即返回
  • 若进程没有任何子进程,则立即出错返回

注意:若接收到信号SIGCHLD后,调用wait,通常wait会立即返回

参数statloc

  • statloc可以为NULL,表明父进程不需要子进程的终止状态。为了防止子进程成为僵尸或者需等待子进程结束
  • 若statloc不是空指针,则进程终止状态就存放在它指向的存储单元中

statloc指向的存储单元,存放了诸多信息,可以通过系统提供的宏获取

说明
WIFEXITED(status) 若为正常终止子进程返回的状态,则为真。 对于这种情况可以执行WEXITSTATUS(status), 取子进程传递给exit、 _exit、 _Exit参数的低8位
WIFSIGNALED(status) 若为异常终止子进程返回的状态,则为真。 对于这种情况可执行WTERMSTG(status), 取使子进程终止的信号编号
WIFSTOPPED(status) 若为当前暂停子进程的返回的状态,则为真。 对于这种情况,可执行WSTOPSIG(status), 取使子进程暂停的信号编号
WIFCONTINUED(status) 若在作业控制暂停后已经继续 的子进程返回了状态,则为真

尽量用自己的通信方式,避免用宏通信

int main(void)
{
	pid_t pid;
	printf("before fork\n");

	if ((pid = fork()) < 0){
		cout << "fork error" << endl;
		return 0;
	}
	else if (pid == 0){
		exit(4);
	}
	else{
		int status;
		wait(&status);	//父进程调用wait会等待子进程死亡
		if (WIFEXITED(status)){
			cout << "exit code: " << WEXITSTATUS(status) << endl;
		}
	}
	return 0;
}

waitpid函数

waitpid函数可用于等待某个特定的进程

如果一个进程有几个子进程,那么只要有一个子进程终止,wait就返回

如何才能等待一个指定的进程终止?

  • 调用wait,然后将其返回的进程ID和所期望的进程ID进行比较
  • 如果ID不一致,则保存该ID,并循环调用wait函数,直到等到所期望的进程ID为止
  • 下一次又想等待某一特定进程时,先查看已终止的进程列表,若其中已有要等待的进程,则无需再调用wait函数
pid_t waitpid(pid_t pid, int *statloc, int options);
  • 成功返回进程ID,失败返回-1
  • statloc:存放子进程终止状态,不关心可以传个0进去

参数pid

  • pid==-1:等待任一子进程,同wait
  • pid>0:等待进程ID为pid的子进程
  • pid==0:等待其组ID等于调用进程组ID的任一子进程
  • pid<-1:等待其组ID等于pid绝对值的任一子进程

参数options:可以为0,也可以是以下常量或运算的结果

  • WCONTINUED
  • WUNTRACED
  • WNOHANG:若pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回0

程序演示waitpid的非阻塞版本,开发时一般使用非阻塞版本

int main(void)
{
	pid_t pid;
	printf("before fork\n");
	if ((pid = fork()) < 0){
		cout << "fork error" << endl;
		return 0;
	}
	else if (pid == 0){
		cout << "child sleep" << endl;
		sleep(5);
		cout << "child exit" << endl;
	}
	else{
		sleep(1);
		if (waitpid(pid, NULL, WNOHANG) == 0){ // 不关心终止状态无阻塞
			cout << "waitpid return 0" << endl;// 等待的子进程还没有死
		}
		wait(NULL);
	}
	return 0;
}

child sleep,waitpid return 0等秒后 child exit

网络中非阻塞重要性,服务器内存管理难,免锁多并发

waitpid函数提供了wait函数没有的三个功能:

  • waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态
  • waitpid提供了一个wait的非阻塞版本。有时用户希望取得一个子进程的状态,但不想阻塞
  • waitpid支持作业控制
int main(void){
    pid_t pid;
    printf("before fork\n"); 
    pid = fork(); // 创建子进程

    if (pid == 0){
        pid = fork(); //创建孙子

        if(pid == 0){
            sleep(2);
            cout << "second child, parent pid = " << getppid() << endl;
        }
        exit(0); //子进程退出,孙进程失去父进程,由init收养
    } 

	waitpid(pid, NULL, 0);        
	return 0;
}

需求:如果一个进程fork了一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死状态直到父进程终止

技巧:调用fork进程两次(不太好,性能不高多创建一个进程)

wait3和wait4

#include <sys/resource.h>
pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
     struct timeval  ru_utime;     /* user time used */
     struct timeval  ru_stime;     /* system time used */
     long            ru_msgsnd;    /* messages sent */
     long            ru_msgrcv;    /* messages received */
     long            ru_nsignals;  /* signals received */     
  • Both return: process ID if OK, 0, or -1 on error.
  • These two functions allows the kernel to return a summary of resource used by the terminated process and all its child processes.
  • The resource info includes such as the amount of user CPU time, the amount of system CPU time, and the like.

exec等函数

  • 进程调用exec等函数用于执行另一个可执行文件

  • 当进程调用一种exec函数时,该进程执行的程序完全替换为新程序

  • 而新程序则从其main函数开始执行

  • exec并不创建新进程,所以前后的进程ID并未改变,exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。将可执行文件里面的内容替换当前地址空间里面的内容

execl函数

int execl(const char *pathname, const char *arg0, .../* (char*)0 */);
  • 出错返回-1,成功不返回值(栈都是新的,无返回地址)
  • pathname:要执行程序的路径名
  • 可变参数:要执行程序的命令行参数,以“(char *)0”结束
#include<iostream>
#include<unistd.h>
#include<sys/wait.h>

using namespace std;

int main()
{
  pid_t pid = fork();
  if(pid == 0)
  {
    cout << "pid = " << getpid() << endl;
    execl("./a.out", "./a.out", "Hello ", "World!", (char *)0);//调用方法
    cout << "back" << endl;	//成功调用不会被执行,因为进程地址空间已经被替换
  }

  wait(NULL);
  return 0;
}

/// a.cpp
#include<iostream>
#include<unistd.h>

using namespace std;

int main(int argc, char *argv[])
{
  cout << "in new program pid = " << getpid() << endl;
  cout << argv[1] << argv[2] << endl;

  sleep(5);

  return 0;
}
pid = 2176
in new program pid = 2176 #没有创建新的进程
Hello World!

以旧的为蓝本,全部填充新内容

其他类exec函数

int execv(const char *pathname, char *const argv[]); //v:vector

int execle(const char *pathname, const char *arg0, .../* (char*)0, char *const envp[] */); // l:list e:environment
//最后一项是一个指向环境字符串指针数组的指针
int execve(const char *pathname, char *const argv[], char *const envp[]);

int execlp(const char *filename, const char *arg0, .../* (char*)0*/);
//指定了要执行的文件名。路径信息:从环境变量PATH中获取
//PATH=/bin:/usr/bin:/usr/local/bin/:.
int execvp(const char *filename, char *const argv[]);

六个函数开头均为exec,所以称为exec类函数

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

Properties that the new program inherits from the calling process.

  • process ID and parent process ID
  • real user ID and real group ID
  • supplementary group IDs process group ID
  • session ID controlling terminal
  • time left until alarm clock current work directory
  • root directory file mode create mask
  • file locks process signal mask
  • pending signals resource limits
  • tms_utime, tms_stime, tms_cutime,tms_cstime

通常,只有execve是内核的系统调用,其他5个都是库函数

graph LR
execlp --build argv-->execvp
execvp --Try each PATH prefix-->execv
execl --bulid argv-->execv
execv --use environ-->execve
execle --bulid argv-->execve
Loading

更改用户ID和组ID

第一种ID:

  • Linux是一个多用户的操作系统。每个用户都有一个ID,用以唯一标识该用户。这个ID,被称为UID。
  • 每个用户都属于某一个组,组也有一个ID。这个ID,被称为组ID,GID。

第二种ID:文件所有者相关

  • 文件所有者ID:拥有某文件的用户的ID
  • 文件所有者组ID:拥有某文件的用户所属组的ID

第三种ID:实际用户ID和实际组ID

  • 进程的实际用户ID:运行该进程的用户的ID
  • 进程的实际组ID:运行该进程的用户所属的组ID

第四种ID:有效用户ID和有效组ID

  • 进程的有效用户ID:用于文件访问权限的检查
  • 进程的有效组ID:大多数情况下,有效用户/组ID=实际用户/组ID

设置用户ID位和设置组ID位

  • 在可执行文件的权限标记中,有一个“设置用户ID位”
  • 若该位被设置,表示:执行该文件时,进程的有效用户ID变为文件的所有者
  • 对于设置组ID位类似

第五种ID:

  • 保存的设置用户ID
  • 保存的设置组ID
  • 上述两者在执行一个程序时(exec)包含了有效用户ID和有效组ID的副本

系统的权限检查是基于用户ID或组ID

当程序需要增加特权,或需要访问当前并不允许访问的资源时,需要更换自己的用户ID或组ID

可以用setuid设置实际用户ID和有效用户ID;setgid设置实际组ID和有效组ID

setuid和setgid函数

int setuid(uid_t uid);
int setgid(gid_t gid);

改变用户/组ID的规则

  • 若进程具有超级用户权限,则setuid将实际用户ID、有效用户ID、保存的设置用户ID设置为uid
  • 若进程没有超级用户权限,但uid等于实际用户ID或保存的设置用户ID,则setuid只将有效用户ID设置为uid,不改变实际用户ID和保存的设置用户ID
  • 若以上条件不满足,返回-1,errno设为EPERM

用户ID和组ID

只有超级用户进程可以更改实际用户ID

  • 实际用户ID是在用户登录时,由login程序设置的
  • login是一个超级用户进程,当它调用setuid时,会设置所有三个用户ID

仅当对程序文件设置了设置用户ID位时,exec才会设置有效用户ID。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置用户ID

保存的设置用户ID是由exec复制有效用户ID而得来的

例子:man联机手册

$ man 3 fopen #调用exec,设置了用户ID位
# real uid = abc
# e有效 uid = man
# s保存的 uid = man(copy from e uid)
#第二步
#想把有效用户ID换成abc 
$ setuid(abc) #满足条件1,可改
#如果是超级用户会把 real/e/s uid全部变成abc
#这时用man的e uid运行,需要满足两个中一个条件:
	#1. 是不是和实际用户ID相等
	#2. 是不是和保存用户ID相等
# real uid = abc
# e uid = abc
# s uid = man
$ setuid(man) #满足条件2,可改

man程序文件是由名为man的用户拥有的,且设置用户ID位已设置。当执行exec此程序时,用户ID:

  • 实际用户ID=我们的用户ID
  • 有效用户ID=man
  • 保存的设置用户ID=man

man程序访问要手册页,而手册页文件是由名为man的用户所拥有的。有效用户ID是man,所以可以访问这些文件

在man代表我们运行任一命令之前(使有效用户ID等于我们的用户ID),它调用setuid(getuid())。(不是超级用户进程)

  • getuid返回实际用户ID,即我们的用户ID
  • 实际用户ID=我们的用户ID
  • 有效用户ID=我们的用户ID
  • 保存的设置用户ID=man

这样,以我们的用户ID作为其有效用户ID而运行。这就意味着能访问的只有我们通常可以访问的,而没有任何额外的权限

当man需要对其手册页进行访问时,又需要将其有效用户ID改为man

  • man调用setuid(man)
  • 实际用户ID=我们的用户ID
  • 有效用户ID=man
  • 保存的设置用户ID=man

由于setuid的参数等于保存的设置用户ID,所以setuid可以成功修改有效用户ID

这就是保存的设置用户ID的作用

ID exec setuid(uid)
set-user-ID off set-user-ID on superuser normal user
real user ID unchanged unchanged set to uid unchanged
effective user ID unchanged Set from user ID of program file set to uid set to uid
saved set-user-ID Copied from effective user ID Copied from effective user ID set to uid unchanged

system函数

用于执行一个shell命令

int system(const char* cmdstring);

cmdstring:shell命令

int main(){
  system("ls -l");
  return 0;
}

system是通过fork、exec、waitpid等实现的,因此有三种返回值

  • 即fork失败,exec失败,waitpid失败

进程会计

进程会计记录:包含命令名,CPU时间总量,用户ID和组ID,启动时间等等

用户标识和进程时间

getlogin函数可以获取当前用户的登录名

char *getlogin();
  • 调用此函数的进程没有连接到用户登录时所用的终端(守护进程),则本函数会失败,返回NULL
  • 这种进程通常称为守护进程
  • 成功返回登录名
//程序演示(4.20)
int main()
{
  cout << getlogin() << endl;
  return 0;
}

进程时间

times函数用于获取墙上时钟时间、用户CPU时间、系统CPU时间

clock_t times(struct tms *buf);

times填写buf指向的tms结构

struct tms {
  clock_t tms_utime; //用户CPU时间
  clock_t tms_stime; //系统CPU时间
  ...............
};

返回值

  • 返回墙上时钟时间
  • 该值相对于过去某个时刻测量的,不能使用其绝对值。只能用相对值

进程关系

进程组

每个进程除了有一个进程ID外,还属于一个进程组

进程组是一个或多个进程的集合。通常,它们与同一作业关联,可以接收来自同一终端的各种信号(如父子进程)

每个进程组有一个唯一的进程组ID

每个进程组都可以有一个组长进程;组长进程的标识是:其进程组ID等于组长进程ID

只要进程组中还有一个进程存在,则进程组就存在,与组长进程存在与否无关

从进程组创建开始,到其中最后一个进程离开为止的时间区间,称为进程组的生存期

进程组中的最后一个进程可以终止,或者转移到另一个进程组

setpgid函数

用于加入一个现有的进程组或者创建一个新进程组

int setpgid(pid_t pid, pid_t pgid);

该函数将进程ID为pid的进程的进程组ID,设置为pgid

  • 若pid=pgid,则pid代表的进程将变为进程组组长

  • 若pid=0,则使用调用者的进程ID

  • 若pgid=0,则由pid指定的进程ID将用作进程组ID

注意:一个进程只能为它自己或它的子进程设置进程组ID。在子进程调用exec函数之后,父进程就不能再改变该子进程的进程组ID

getpgid函数

用于获取调用进程的进程组ID

pid_t getpgrp();

返回调用进程的进程组ID

会话

会话是一个或多个进程组的集合

一次登录形成一个会话

Shell上的一条命令形成一个进程组

proc1 | proc2 &(开两个终端,观察组长 会话ID)

proc3 | proc4 | proc5 (命令 ps –xj;程序4.21;在一个终端中分别前后台启动)

//test与test1相同
int main(){
  sleep(600);
  return 0;
}
$ ./test | ./test1 & #以后台方式启动两个程序
#父进程  进程id	进程组ID  会话ID
PPID	PID		PGID	SID
1465	1612	1612	1612			-bash #会话
1612	2279	2279	1612			./test #test为组长ID
1612	2280	2279	1612			./test1 #和test来源于-bash会话

Controlling terminal 控制终端

There are a few other characteristics of session and group:

  • A session can have a single controlling terminal. 一个会话有一个控制终端
  • The session leader that establishes the connection to the controlling terminal is called the controlling process.
  • 控制进程:会话首进程会建立到控制终端的链接
  • The process groups within a session can be divided into a single foreground process and one or more background group.
  • 是否拥有终端分为两类:前台进程组(只有一个),后台进程组
  • If a session has a controlling terminal, then it has a single foreground process group, and all other group in the session are background group.
  • 一个会话有控制终端,有前台进程组,其他属于后台进程组
  • Whenever we type our terminal’s interrupt key (often delete or Ctrl+C ) or quit key (often Ctrl-), this causes either the interrupt signal or the quit signal to be sent to all processes in the foreground process group.
  • 退出信号会送给前台进程组所以进程
  • If a modem disconnect is detected by the terminal interface, the hang-up signal is sent to the controlling process (the session leader).

需要注意的是:会话中的前台或后台进程组是针对该会话是否拥有控制终端而言。也即,如果一个会话没有控制终端,则会话中所有进程组均属“后台”。

graph BT
a[Controlling terminal] -->|Modem disconnect hangup_signal| b[a]
subgraph background process group
	b[login shell]
	b1[Session leader=Controlling leader]
end
a -->c
subgraph background process group
	c[proc1]
	c2[proc2]
end
a -->|Terminal input and terminal-generated signal|d
subgraph foreground process group
	d[proc3]
	d2[proc4]
	d3[proc5]
end
Loading

setsid

pid_t setsid();

非组长进程调用此函数,会创建一个新会话,导致三件事:

  • 该进程变成新会话首进程,即会话首进程是创建该会话的进程
  • 该进程成为一个新进程组的组长进程
  • 该进程没有控制终端

组长进程调用此函数,返回出错-1

pid_t getsid(pid_t pid);

返回会话首进程的进程组ID,即会话ID

程序演示

会话首进程失去终端(程序4.22)

  • Test中,Ctrl C只有父进程退出
  • Test1中,Ctrl C导致父子进程退出
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
int main(void)
{
    pid_t pid;
    int c;

    if ((pid = fork()) < 0)
    {
        cout << "fork error." << endl;
        return 0;
    }
    else if (pid == 0)
    {
        printf("Before setsid: child pid: %d, ppid: %d, pgrp: %d, sid: %d\n", getpid(), getppid(), getpgrp(), getsid(0));

        setsid();//创建一个新会话 //test1.cpp中注释了这一行

        printf("After setsid: child pid: %d, ppid: %d, pgrp: %d, sid: %d\n", getpid(), getppid(), getpgrp(), getsid(0));

        pause();//需要ctrl+c发信号
        exit(0);
    }

    printf("parent pid: %d, ppid: %d, pgrp: %d, sid: %d\n", getpid(), getppid(), getpgrp(), getsid(0));

    pause();
    exit(0);
}
$./a.out 
parent pid 2313 ppid 1612 grp 2313 sid 1612 #父进程是1612是shell进程,组长进程
Before setsid chlid pid: 2314 ppid 1613 grp 2313 sid 1612
After setsid child pid: 2314 ppid 1613 grp 2314 sid 2314 #进程组发生改变,子进程变成组长进程,形成新的会话ID
^C
$ps -xj
1	2314	2314	2314 ?	-1 Ss	0 0:00 ./a.out
#按Ctrl C子进程并没有退出,该进程没有控制终端,无法接收^C
$kill 2314
//如果注释掉 setsid()
	if((pid=fork())<0){
        cout << "fork error." << endl;
        return 0;
    }
    else if(pid==0){
        printf("Before setsid: child pid: %d, ppid: %d, pgrp: %d, sid: %d\n",getpid(),getppid(),getpgrp(),getsid(0));

        //setsid();//创建一个新会话 如果将这里注释掉

        printf("After setsid: child pid: %d, ppid: %d, pgrp: %d, sid: %d\n",getpid(),getppid(),getpgrp(),getsid(0));
        pause();//需要ctrl+c发信号
        exit(0);
    }
    printf("parent pid: %d, ppid: %d, pgrp: %d, sid: %d\n",getpid(),getppid(),getpgrp(),getsid(0));
    pause();
    exit(0);
}

ctrl C后父子进程都退出了,而且子进程不会变化