通常,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函数提供了进行这样工作的机会
它允许用户注册若干终止处理函数,当进程终止时,这些终止处理函数将会被自动调用
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 数组传入函数方法
- 数组,长度
- 数组头指针,数组尾指针
- 给头指针(结尾放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;
}
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);
分配内存想释放不能释放才是内存泄漏,分配不释放不属于内存泄漏
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中的各类变量的值是否改变回原来的值?
- 全局变量、静态变量、易失变量不受优化的影响
- 在优化的版本,自动变量和寄存器变量存储在寄存器中
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
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:
- A soft limit can be changed by any process to a value less than or equal to its hard limit. 软的限制可以修改但小于等于硬限制
- Any process can lower its hard limit to a value greater than or equal to its soft limits. 进程可以降低硬限制,但大于等于软的限制
- 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函数创建一个新进程
新进程被称为子进程
pid_t fork(void);
- fork函数调用一次,但是返回两次
- 在子进程中返回0,在父进程中返回子进程ID,出错返回-1
- 通过返回值,可以确定是在父进程还是子进程中
父进程 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的一个特性:父进程的所有打开文件描述符,都被复制到子进程中。
父子进程对同一文件使用了一个文件偏移量
上例中,父进程等待了子进程两秒钟,所以他们的输出才没有混乱;否则有可能出现乱序
文件描述符的常见处理方式
- 父进程等待子进程完成。父进程无需对描述符做任何处理,当子进程终止后,文件偏移量已经得到了相应的更新
- 父子进程各自执行不同的程序段,各自关闭文件描述符
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返回后,立即调用exec执行另外一个程序
代码4.10:封装fork(面向对象,面向API....)(研究生课程)
-
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操作,结果改变了父进程中的变量值。原因?父子是一个共享的关系
正常终止
- 从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函数,获取其终止状态
当一个进程正常获知异常终止时,内核就向其父进程发送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函数可用于等待某个特定的进程
如果一个进程有几个子进程,那么只要有一个子进程终止,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进程两次(不太好,性能不高多创建一个进程)
#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函数时,该进程执行的程序完全替换为新程序
-
而新程序则从其main函数开始执行
-
exec并不创建新进程,所以前后的进程ID并未改变,exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。将可执行文件里面的内容替换当前地址空间里面的内容
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!
以旧的为蓝本,全部填充新内容
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
第一种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
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是在用户登录时,由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 |
用于执行一个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
只要进程组中还有一个进程存在,则进程组就存在,与组长进程存在与否无关
从进程组创建开始,到其中最后一个进程离开为止的时间区间,称为进程组的生存期
进程组中的最后一个进程可以终止,或者转移到另一个进程组
用于加入一个现有的进程组或者创建一个新进程组
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
用于获取调用进程的进程组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会话
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
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后父子进程都退出了,而且子进程不会变化