文件描述符通常是一个非负整数,用以标识一个特定进程正在访问的文件
运行一个新程序,所有shell会为其打开3个文件描述符:标准输入/输出/错误
不带缓冲的I/O
标准I/O
#include "apue.h"
#include
// 相当于模拟了一个shell的处理过程
int
main(void)
{
char buf[MAXLINE]; /* from apue.h */
pid_t pid;
int status;
printf("%% "); /* print prompt (printf requires %% to print %) */
while (fgets(buf, MAXLINE, stdin) != NULL) {
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = 0; /* replace newline with null */
if ((pid = fork()) < 0) { //创建子进程
err_sys("fork error");
} else if (pid == 0) { /* child */
execlp(buf, buf, (char *)0); //并且执行,第八章详述
err_ret("couldn't execute: %s", buf);
exit(127);
}
/* parent */
if ((pid = waitpid(pid, &status, 0)) < 0) //父进程等待子进程终止
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
在编译运行这个程序时遇到一些问题:
1.问题描述
gcc -Wall shell1.c
显示找不到apue.h,编译终止。
解决方案
于是发现apue.h在随书附带代码的include文件夹中,此外还需要将lib文件夹中的error.c文件一起复制到系统的/usr/include中,然后编辑apue.c在末行添加#include “error.c”.
实际操作时发现复制文件到/usr/include下是需要root权限的,另外编辑apue.c也是需要root权限的。
#include "apue.h"
#include
int
main(int argc, char *argv[])
{
fprintf(stderr, "EACCES: %s\n", strerror(EACCES));
errno = ENOENT;
perror(argv[0]);
exit(0);
}
strerror 和perror两个函数输出程序运行的错误信息
errno常量记录出错状态,大约15种
#include "apue.h"
int
main(void)
{
printf("uid = %d, gid = %d\n", getuid(), getgid());
exit(0);
}
每个用户都有一个用户标识(uid)和至少一个组标识(gid)
include "apue.h"
#include
static void sig_int(int); /* our signal-catching function */
int
main(void)
{
char buf[MAXLINE]; /* from apue.h */
pid_t pid;
int status;
if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal error");
printf("%% "); /* print prompt (printf requires %% to print %) */
while (fgets(buf, MAXLINE, stdin) != NULL) {
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = 0; /* replace newline with null */
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */
execlp(buf, buf, (char *)0);
err_ret("couldn't execute: %s", buf);
exit(127);
}
/* parent */
if ((pid = waitpid(pid, &status, 0)) < 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
void
sig_int(int signo)
{
printf("interrupt\n%% ");
}
信号是用于通知进程发生了某种情况,进程对信号有三种处理方式:
历史上使用过两种时间值:日历时间(time_t)、进程时间(clock_t)
当度量一个进程执行时间时,unix为每个进程维护了3个时间:
系统调用处于不断发展之中,现在的FreeBSD 8.0中已经有450个函数调用了
Unix的做法是在标准C函数库中设置一个与系统调用具有同样名字的函数,用户进程调用C库函数。 c库函数又用系统要求的方式调用相应的内核服务,要注意:
总结起来就是,系统服务都是一些非常基本的模块,C库在此基础上进行了适度的封装,这也就是为什么C语言是接近系统底层的语言。
在过去的将近25年时间,人们为了UNIX的标准化做出了种种努力,这使得程序在不同版本的UNIX系统之间的移植相当容易。
1989年,C语言首个标准得到批准,其为C89。次年,一个带有小改动的版本标准被批准其为C90。因此,C89和C90通常指同一种语言。在2000年三月,ANSI采纳了ISO/IEC 9899:1999标准。这个标准通常指C99。在2011年12月,ANSI采纳了ISO/IEC 9899:2011标准。这个标准通常即C11,它是C程序语言的现行标准。
按照ISO C标准定义的头文件将C语言公用函数库划分成了24个部分。POSIX.1标准包括这些头文件以及一些额外的头文件。
POSIX是由IEEE制订的一系列标准,其指的是可移植操作系统接口,它说明的是接口而不是实现。
Single UNIX Specification简称SUS,它是POSIX.1标准的一个超集。
ISO C、IEEE POSIX、Single UNIX Specification是三个不同的组织,第一个组织负责对C语言进行标准化,后两个组织负责对UNIX系统接口进行标准化。这三个组织制定了概念上的规范,但实现是由厂商进行的。几个知名实现发行版如下:
UNIX系统中既有编译器的限制,又有UNIX实现(与编译程序无关)有关的限制。这两种限制有交集部分,也有差集部分,比如int类型的字节长度,和编译器有关,和UNIX系统无关,但通常编译器会参考操作系统的实现来决定。而程序能打开文件描述符的数量和UNIX实现有关,而和编译器无关。对于操作系统的种种限制,UNIX系统提供了3个库函数来查看。其头文件及函数原型如下:
#include
long int sysconf (int name);
long int pathconf (const char* path, int name);
long int fpathconf (int fd, int name);
对于上述3个函数,成功时返回相应值,失败则返回-1。对于pathconf( )、fpathconf( )函数来说,第一个参数并不是总是有意义的,当需要查询一个文件的链接数时,第一个参数总是需要的,当查询系统的文件名长度限制时,第一个参数是无意义的,只要不为空即可。
#include
int stat(const char * restrict pathname, struct stat*restrict buf);
int fstat(int fd, struct stat* buf);
int lstat(const char* restrict pathname,struct stat *restrict buf);
int fstatat(int fd,const char*restrict pathname,struct stat*restrict buf,int flag); //返回文件或者目录的信息结构
参数:
pathname
:文件或者目录的名字buf
:存放信息结构的缓冲区fd
:打开的文件描述符
fstat
,该文件就是待查看信息的文件fstatat
,该文件是并不是待查看信息的文件。待查看信息的文件时已该fd
对于的目录相对路径定位的flag
:控制着fstatat
函数是否跟随一个符号链接。对于fstatat
函数:
fd
和pathname
共同决定的。
pathname
是个绝对路径,则忽略fd
参数pathname
是个相对路径路径,且 fd=AT_FDCWD
,则在当前工作目录的路径下查找pathname
pathname
是个相对路径路径,且 fd!=AT_FDCWD
,则在fd
对应的打开目录下查找pathname
flag
:控制着fstatat
函数是否跟随一个符号链接。当!AT_SYMLINK_FOLLOW
标志被设置时,查看的是pathname
(如果它是个符号链接)本身的信息;否则默认查看的是pathname
(如果它是个符号链接)链接引用的文件的信息。返回值:
注意:
lstat
类似于stat
,但是当pathname
是个符号链接时,lstat
查看的是该符号链接的有关信息;而stat
是查看该符号链接引用的文件的信息。ubuntu 16.04
上,虽然有 AT_SYMLINK_NOFOLLOW
这个常量,但是不支持。必须用 !AT_SYMLINK_FOLLOW
。其常量定义为:
AT_SYMLINK_FOLLOW
: 1024 (有效)!AT_SYMLINK_FOLLOW
: 0(有效)AT_SYMLINK_NOFOLLOW
: 256(无效)AT_SYMLINK_FOLLOW
: -1025(无效)stat
数据结构:其定义可能与具体操作系统相关,但是基本形式为:
struct stat{
mode_t st_mode; //文件权限和类型信息
ino_t st_ino; //i-node 号
dev_t st_dev; // 设备号
dev_t st_rdev; // 特殊文件的设备号
nlink_t st_nlink; // 硬链接数量
uid_t st_uid; // owner 的用户ID
gid_t st_gid; // owner 的组ID
off_t st_size; //对普通文件,它是文件字节大小
struct timespec st_atime; // 上次访问时间
struct timespec st_mtile; // 上次修改时间
struct timespec st_ctime; // 上次文件状态改变的时间
blksize_t st_blksize; // 最佳的 I/O block 大小
blkcnt_t st_blocks; //分配的磁盘块数量
}
其中timespec
结构与具体操作系统相关,但是至少包括下面两个字段:
struct timespec{
time_t tv_sec; // 秒
long tv_nsec; //纳秒
}
普通文件:最常见的文件类型,这种文件包含了某种形式的数据。至于这种数据是二进制还是文本,对内核无区别。普通文件的内容解释由具体的应用程序进行。
目录文件:这种文件包含了其他文件的名字,以及指向这些文件有关信息的指针。
块特殊文件:这种类型的文件提供对设备(如磁盘)带缓冲的访问。每次访问以固定长度为单位进行。
字符特殊文件:这种类型的文件提供对设备不带缓冲的访问,每次访问长度可变。
系统的所有设备,要么是字符特殊文件,要么是块特殊文件
FIFO
:这种类型的文件用于进程间通信,有时也称为命名管道
套接字:这种类型的文件用于进程间的网络通信(也可用于单机上进程的非网络通信)
符号链接:这种类型的文件指向另一个文件
文件类型信息存放在stat.st_mode
成员中,可以用下列的宏测试文件类型:
S_ISREG()
:测试是否普通文件S_ISDIR()
:测试是否目录文件S_ISCHR()
:测试是否字符特殊文件S_ISBLK()
:测试是否块特殊文件S_ISFIFO()
:测试是否FIFO
S_ISLNK()
:测试是否符号链接文件S_ISSOCK()
:测试是否套接字另外 POSIX.1
允许将进程间通信对象说明为文件。但是下面的宏测试的不是stat.st_mode
,而是stat*
(stat
指针):
S_TYPEISMQ()
:测试是否消息队列S_TYPEISSEM()
:测试是否信号量S_TYPEISSHM()
:测试是否共享存储对象与一个进程有关的ID有很多:
exec
函数保存每个文件都有一个所有者和组所有者,分别有 stat.st_uid
和stat.st_gid
指定。当一个文件为可执行文件时,如果执行这个文件,那么进程的有效用户ID就是实际用户ID,有效组ID就是实际组ID,除了下面的情况:
stat.st_mode
中设置了一个特殊标志:设置用户ID位时,则将进程的有效用户ID设置为文件所有者的用户IDstat.st_mode
中设置了一个特殊标志:设置组ID位时,则将进程的有效组ID设置为文件所有者的组ID任何进程都是由可执行文件被执行而得到。因此位于磁盘上的可执行文件的所属的用户ID和组ID会影响到进程的用户ID和组ID
如果某个可执行文件所有者是root
,且该文件的设置用户ID位已经被设置,那么无论谁执行这个可执行文件时,该可执行文件产生的进程就具有超级用户权限。
设置用户ID位、设置组ID位 都包含在stat.st_mode
中,可以通过下列两个宏测试:
S_ISUID()
:测试是否设置了设置用户ID位S_ISGID()
:测试是否设置了设置组ID位S_IRUSR
:用户读S_IWUSR
:用户写S_IXUSR
:用户执行S_IRGRP
:组读S_IWGRP
:组写S_IXGRP
:组执行S_IROTH
:其他读S_IWOTH
:其他写S_IXOTH
:其他执行访问权限规则:
当用名字pathname
打开任何一个类型的文件时,对pathname
中包含的每一个目录,包括pathname
可能隐含的当前工作目录都应该具有执行权限
因此目录的执行权限位也称之为搜索位
对一个文件的读权限决定了我们能否打开现有文件进行读操作
对一个文件的写权限决定了我们能否打开现有文件进行写操作
如果你在open
函数中对一个文件指定了O_TRUNC
标志,则必须对该文件具有写权限
为了在一个目录中常见一个新文件,必须对该目录具有写权限和执行权限
为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限。对该文件本身没有权限的限制
如果用7个exec
函数中的任何一个执行某个文件,则必须对该文件具有执行权限,且该文件必须是个普通文件
进程每次打开、创建、删除一个文件时,内核就进行文件访问权限测试。这种测试如下:
只要有一个权限通过,则不再进行测试。若所有权限都不通过,则不允许访问。
open
或者creat
创建一个新文件时:新文件的用户ID被设置为进程的有效用户ID
新文件的组ID可以有两个值之一:
具体选择哪个,由具体操作系统决定
#include
int access(const char *pathname ,int mode);
int faccessat(int fd, const chae *pathname, int mode, int flag);
成功返回0,失败返回-1
#include
mode_t umask(mode_t cmask);
返回值:之前的文件模式创建屏蔽字
#include
int chmod(const char *pathname,mode_t mode);
int fchmod(int fd,mode_t mode);
int fchmodat(int fd,mode_t mode);
int fchmodat(int fd,const char *pathname,mode_t mode,int flag);
成功返回0,失败返回-1
#include
int chown(const char*j pathname,uid_t owner,gid_t group);
int fchown(int fd,uid_t owner,gid_t group);
int fchownat(int fd,const char *pathname,uid_t owner,gid_t group,int flag);
int lchown(const char *pathname,uid_t owner,gid_t group);
成功返回0,失败返回-1
基于BSD的系统一直规定只有超级用户才可以更改用户ID,但system V允许任意用户更改他们所拥有的文件的组ID,但只能改到你所属的组。unistd.h中的_POSIX_CHOWM_RESTRICTED常量可以触发这一规定。
stat结构体中的st_size表示以字节为单位的文件长度,此字段只对普通文件,目录文件,和符号链接有意义。
大多数Unix系统提供字段st_blksize和st_blocks来表明当前系统文件I/O较合适的块长度和块数。
文件的空洞:文件的物理结构到底是咋整的?
函数truncate和ftruncate可以实现在文件任意偏移处截断文件
将一个文件截断为0,可以有两种方法:在打开文件时使用O_TRUNC标志;
#include
int truncate(const char *pathname,off_t length);
int ftruncate(int fd,off_t length);
首先一块磁盘可以分为几个分区,每个分区内可以建立起不同的文件系统(Solaris支持多种不同类型的磁盘文件,传统基于BSD的unix采用UFS,PCFS,HSFS等)
文件系统是怎样建立起来呢?自举块+超级块+各柱面组,每个柱面组里面分为i节点组,和数据块(目录块和文件块)
i节点在nuix文件系统中地位很重要:
目录文件的链接计数:
即便是一个空目录也会有两个计数,一个是子目录.
,一个是命名他的目录项;
然后就是目录中的每一个子目录都会使该目录链接计数加1
#include
int link(const char *existingpath,const char *newpath);
int linkat(int efd,const char *existingpath,int nfd,const char *newpath,int flag);
int unlink(const char *pathname);
int unlinkat(int fd,const char *pathname,int flag);
成功返回0,失败返回-1
注意:
#include
int rename(const char *oldname,const char *newname);
int rename(int oldfd,const cahr *oldname,int newfd,int newfd,const cahr *newname)
8种进程终止方式,其中5种为正常终止:
异常终止的3种方法:
1.退出函数
#include
void exit(int status);
void _Exit(int status);
#include ;
void _exit(int status);
3个函数用于正常终止一个程序,_exit和 _Exit立即进入内核,exit则先执行一些清理处理,然后返回内核。
#include
int atexit(void (*func)(void));
exit函数不是要进行清理处理嘛,这些清理处理程序称为终止处理程序,每个被调用的终止处理程序都要在atexit处登记。
就是说shell可以在执行程序时将参数以命令行的方式传递给程序
环境表是一个字符指针数组,全局变量environ包含了该指针数组的地址
正文段(.text) 数据段(.date) 未初始化数据(.bss)堆 栈
32位x86处理器一般是这样的,正文从0x08048000开始,栈底在0xC0000000之下
当然一个可执行文件还包括符号表,调试信息段,动态库段等,但是这些不装载到进程执行的程序映像中。
共享库的优点是可以节省可执行文件体积,共性库新老版本升级时只要接口不变就不用重新连接,缺点是增加运行时的时间开销
#include
void *malloc(size_t size);
void *calloc(size_t nobj,size_t size);
void *realloc(void *ptr,size_t newsize);
这些函数都是基于sbrk(2)系统调用实现,而且都返回通用指针 *void
替代的存储分配程序
各个环境下都有实现
环境变量的获取函数
#include
char *getenv(const char *name);
环境变量的设置函数
#include
int putnev(char *str);
成功返回0,出错返回非0
int setenv(const char *name,const char *value,int rewrite);
int unsetenv(const char *name);
成功返回0,出错返回-1
环境变量存在进程地址空间顶部,也就是栈空间之上,这里的空间使用需要很谨慎。
#include
int setjmp(jmp_buf env);
void longjmp(jmp_buf env,int val);
配合使用的,当直接调用时返回0,从longjmp返回时为val
setjmp函数的参数是一个数组,其中存放在调用longjmp时能用来回复栈状态的所有信息(setjmp执行在前怎么能记下以后调用的对战情况呢?)
当执行longjmp跳转后,存储器中的变量将具有longjmp时的值,但是CPU和浮点寄存器的变量则恢复为调用setjmp时的值。
自动变量的潜在问题:
就是一个函数一旦返回,那么他原来的栈帧将不能为他所用,但是考虑这样一种情况,如果这个函数在栈帧中开辟了一个空间当作流缓冲,他退出后,标准I/o仍然在使用这块流缓冲区,就会产生冲突,
如何避免?想这样的缓冲区就不应该建在栈空间,而是应该在全局存储空间。
#include
int getrlimit(int resource,struct rlimit *rlptr);
int setrlimit(int resource,const struct rlimit *rlptr)
成功返回0,出错返回非0
struct rlimit{
rlim_t rlim_cur; //软限制
rlim_t rlim_max; //硬限制
};
两个函数指向资源和结构体,
资源的限制子进程继承父进程
每个进程都有一个非负整数表示的唯一进程 ID
。
ID
各不相同A
终止后,它的进程 ID
可以复用
B
的ID
不同于最近终止的进程A
的ID
ID
为0的进程通常是调度进程,也称作交换进程。该进程是操作系统内核的一部分,并不执行任何磁盘上的程序,因此也称作是系统进程ID
为1的进程通常是init
进程,在自举过程结束时由内核调用。
/etc/init
,在较新的版本中是/sbin/init
文件/etc/rc*
文件,/etc/inittab
文件以及/etc/init.d
中的文件),并经系统引导到一个状态获取进程标识
#include
pid_t getpid(void); // 返回值:调用进程的进程ID
pid_t getppid(void); // 返回值:调用进程的父进程ID
uid_t getuid(void); // 返回值:返回进程的实际用户ID
uid_t geteuid(void); // 返回值:返回进程的有效用户ID
gid_t getgid(void); // 返回值:返回进程的实际组ID
gid_t getegid(void); // 返回值:返回进程的有效组ID
这些函数都没有出错返回
#include
pid_t fork(void);
子进程返回0,父进程返回子进程ID
注意:
如果fork
调用成功,则它被调用一次,但是返回两次。 两次返回的区别是:子进程的返回值是0,父进程的返回值是新建子进程的进程ID
getpid
知道它的进程ID
,通过getppid
知道它的父进程的ID
ID
的理由:一个进程的子进程可以有多个,但是并没有函数可以获取它的子进程的ID
子进程是父进程的一份一模一样的拷贝,如子进程获取了父进程数据空间、堆、栈的副本。
子进程和父进程都从fork
调用之后的指令开始执行。也就是子进程从出生开始,就跟父进程处于同样的状态
由于创建子进程的目的通常是为了完成某个任务,因此fork
之后经常跟随exec
,所以很多操作系统的实现并不执行一个父进程数据段、堆和栈的完全拷贝,而是使用写时赋值技术(copy-on-write:COW
)
通常fork
之后,是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的进程调度算法
注意标准IO
库的跨fork
行为。由于标准IO
库是带缓冲的,因此在fork
调用之后,这些缓冲的数据也被拷贝到子进程中
父进程的所有打开的文件描述符都被复制到子进程中。父进程和子进程每个相同的打开描述符共享同一个文件表项
fork
之后的任务就是等待子进程完成,而不作任何其他的事情,则父进程和子进程无需对打开的文件描述符做任何处理。因为此时只有子进程处理文件fork
之后,父进程与子进程都有自己的任务要处理,则此时父进程和子进程需要各自关闭它们不需要使用的文件描述符,从而避免干扰对方的文件操作除了打开的文件描述符之外,子进程还继承了父进程的下列属性:实际用户ID
、实际组ID
、有效用户ID
、有效组ID
、附属组ID
、进程组ID
、会话ID
、控制终端、设置用户ID
标志和设置组ID
标志、当前工作目录、根目录、文件模式创建屏蔽字、信号屏蔽和信号处理、对任一打开文件描述符的执行时关闭标志、环境、连接的共享存储段、存储映像、资源限制
父进程和子进程的区别为:
fork
返回值不同ID
不同ID
不同tms_utime,tms_stime,tms_cutime,tms_ustime
的值设置为0fork
失败的零个主要原因:
ID
的进程总数超过了系统的限制(CHILD_MAX
规定了每个实际用户ID
在任何时刻拥有的最大进程数)示例: 在main
函数中调用test_fork
函数:
void test_fork()
{
M_TRACE("--------- Begin test_fork() ---------\n");
assert(prepare_file("test","abc",3,S_IRWXU)==0);
int fd=My_open("test",O_RDWR);
if(-1==fd)
{
un_prepare_file("test");
M_TRACE("--------- End test_fork() ---------\n\n");
return;
}
//****** 打开文件成功 *************//
pid_t id=fork();
if(0==id)
{ // child 1
prgress_func(fd,"**********In Child 1***********");
_exit(0);
}
sleep(2); // 确保父进程在子进程之后执行
id=fork();
printf("This is in the second fork\n");
if(0==id)
{// child 2
prgress_func(fd,"**********In Child 2***********");
_exit(0);
}
sleep(2); // 确保父进程在子进程之后执行
prgress_func(fd,"**********In Parent***********");
close(fd);
un_prepare_file("test");
M_TRACE("--------- End test_fork() ---------\n\n");
}
可以看出:
IO
库是带缓冲的,因此在fork
调用之后,这些缓冲的数据也被拷贝到子进程中,因此"This is in the second fork"
被输出两次fork
有两种用法:
fork
并使子进程处理请求shell
是很常见。此时子进程从fork
返回之后立即调用exec
vfork
函数的调用序列和返回值与fork
相同,但是二者语义不同:
vfork
用于创建一个新进程,该新进程的目的是exec
一个新程序,所以vfork
并不将父进程的地址空间拷贝到子进程中。
vfork
的做法是:在调用exec
或者exit
之前,子进程在父进程的空间中运行所以在
exec
或者exit
之前,子进程可以篡改父进程的数据空间
vfork
保证子进程优先运行,在子进程调用exec
或者exit
之后父进程才可能被调度运行
当子进程调用
exec
或者exit
中的任何一个时,父进程会恢复运行,在此之前内核会使父进程处于休眠状态
在main
函数中调用test_vfork
函数:
void test_vfork()
{
M_TRACE("--------- Begin test_vfork() ---------\n");
assert(prepare_file("test","abc",3,S_IRWXU)==0);
int fd=My_open("test",O_RDWR);
if(-1==fd)
{
un_prepare_file("test");
M_TRACE("--------- End test_fork() ---------\n\n");
return;
}
//****** 打开文件成功 *************//
int i=0; // 用于测试父子进程是否共享同一个子空间
int id=vfork();
if(0==id)
{//child
// fcntl_lock(fd); // 加锁
printf("*********** In Child ***********\n");
print_pid_ppid();
printf("i=%d\n",i);
i=999;
printf("*********** In Child ***********\n");
// fcntl_unlock(fd); // 解锁
_exit(0);
}else
{//parent
// fcntl_lock(fd); // 加锁
printf("*********** In Parent ***********\n");
print_pid_ppid();
printf("i=%d\n",i);
printf("*********** In Parent ***********\n");
// fcntl_unlock(fd); // 解锁
}
close(fd);
un_prepare_file("test");
M_TRACE("--------- End test_vfork() ---------\n\n");
}
可以看出:
_exit(0)
之前,父进程被阻塞;当子进程调用_exit(0)
之后,父进程才开始执行如果我们通过加锁让父进程先获得锁,则结果如下:
可以看出:虽然我们期望父进程先执行(因为父进程先获得锁?),但是实际上仍然是子进程先执行。vfork
直接让父进程处于未就绪的状态,从而不会去获取记录锁。只有当子进程执行完_exit(0)
时,父进程才就绪。
进程有 8 种方式使得进程终止,其中 5 种为正常终止,3 种异常终止:
main
函数返回,等效于exit
exit
函数。exit
会调用各终止处理程序,然后关闭所有标准IO流_exit
函数或者_Exit
函数。它们不运行终止处理程序,也不冲洗标志IO流pthread_exit
函数。进程也是以终止状态 0 返回的abort
函数。它产生SIGABRT
信号更进一步的:
不管进程如何终止,最后都会执行内核中的同一段代码:这段代码为相应进程关闭所有打开的描述符(不仅仅是文件描述符),释放它所使用的内存。
不管进程如何终止,我们需要有一种方法来通知父进程,本进程是如何终止的。
exit,_exit,_Exit
这三种情况:将本进程的退出状态作为参数传给函数,并且在最后调用_exit
时,内核将退出状态转换成终止状态
exit
函数和_Exit
函数最终调用的是_exit
函数
在任意一种情况下,终止进程的父进程都能够用wait
或者waitpid
函数取得终止状态。然后父进程能够检测终止状态。如果发现子进程是正常终止,则可以从终止状态中提取出退出状态
如果父进程在子进程之前终止,那么内核会将该子进程的父进程改变为init
进程,称作由init
进程收养。其原理为:
ID
就改为 1这种方式确保了每个进程都有一个父进程
内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait
函数或者waitpid
函数时,可以得到这些信息。
ID
、终止进程的终止状态、终止进程的使用的CPU时间总量ps
命令中显示为Z
wait
函数或者waitpid
函数读取终止进程的残留信息init
超级进程,它被设计成:任何时候只要有一个子进程终止,就立即调用wait
函数取得其终止状态。这种做法防止系统中塞满了僵死进程当一个进程终止时,内核就向其父进程发送SIGCHLD
信号。这种信号是一个异步信号,因为该信号可能在任何时间发出
#include
pid_t wait(int *staloc);
pid_t waitpid(pid_t pid,int *staloc,int options);
成功返回进程ID,失败返回0或-1
参数:
staloc
:存放子进程终止状态的缓冲区的地址。如果你不关心子进程的终止状态,则可以设它为空指针NULL
对于waitpid
函数:
pid
:
pid==-1
:则等待任意一个子进程终止pid>0
:则等待进程ID
等于pid
的那个子进程终止pid==0
:则等待组ID
等于调用进程组ID
的任一子进程终止pid<0
:等待组ID
等于pid
绝对值的任一子进程终止options
:或者是0,或者是下列常量按位或的结果:
WNOHANG
:没有指定的子进程终止时,并不阻塞程序的执行WUNTRACED
:执行作业控制。若操作系统支持作业控制,则由pid
指定的任一子进程在停止后已经继续,但其状态尚未报告,则返回其状态WCONTINUED
:执行作业控制。若操作系统支持作业控制,则由pid
指定的任一子进程已处于停止状态,并且其状态自停止以来尚未报告过,则返回其状态进程的停止状态:类似于暂停。它不同于终止状态
这两个函数返回值的整形状态字(pid_t)是由实现定义,其中某些位表示退出状态(正常退出),其他位标识信号编号(异常返回),有一位指示是否产生了core文件。根据这个状态字再调用不同的宏来实现对子进程状态的解析。
注意:
wait
的语义是等待任何一个子进程终止:
waitpid
的语义是等待指定的子进程终止:
options
指定为WNOHANG
,则waitpid
并不阻塞,而是立即返回 0options
未指定为WNOHANG
,则waitpid
阻塞pid
的子进程已终止,正在等待父进程获取其终止状态,则当前进程取得该子进程的终止状态并立即返回pid
有问题(如不存在,或者不是当前进程的子进程),则立即出错返回对于出错的情况:
wait
出错的原因是:
waitpid
出错的原因是:
pid
指定的进程不是调用进程的子进程可以通过宏从终止状态中取得退出状态以及终止原因等:
WIFEXITED(status)
:如果子进程正常终止,则为真。此时可以执行WEXITSTATUS(status)
获取子进程的退出状态的低 8 位WIFSIGNALED(status)
:如果子进程异常终止,则为真。此时可以执行WTERMSIG(status)
获取使得子进程终止的信号编号WIFSTOPPED(status)
:如果子进程的当前状态为暂停,则为真。此时可执行WSTOPSIG(status)
获取使得子进程暂停的信号编号WIFCONTINUED(status)
:如果子进程在暂停后已经继续执行了,则为真。在main
函数中调用test_wait_waitpid
函数
void test_wait_waitpid()
{
M_TRACE("--------- Begin test_wait_waitpid() ---------\n");
assert(prepare_file("test","abc",3,S_IRWXU)==0);
int fd=My_open("test",O_RDWR);
if(-1==fd)
{
un_prepare_file("test");
M_TRACE("--------- End test_fork() ---------\n\n");
return;
}
//****** 打开文件成功 *************//
prgress_func(fd,"**********Parent***********");
if(0!=child_exit(fd,100))
{// parent
sleep(1); //确保父进程稍后执行
if(0!=child_abort(fd))
{//parent
sleep(1); //确保父进程稍后执行
if(0!=child_signal(fd))
{
sleep(1); //确保父进程稍后执行
check_wait(); //only wait at parent (二选一)
// check_waitpid(); // only wait at parent (二选一)
close(fd);
un_prepare_file("test");
M_TRACE("--------- End test_wait_waitpid() ---------\n\n");
}
}
}
}
子进程的结束顺序跟它们派生的顺序没有什么关系。wait
只会处理最先结束的子进程
调用了_exit
的子进程,属于正常终止;调用了abort
和被信号终止的子进程属于异常终止
通过waitpid
可以严格控制取得终止子进程状态的顺序
通过waitpid
依次等待所有的子进程,可以确保父进程是最后一个结束的
waitid
函数:它类似waitpid
,但是提供了更灵活的参数
#include
int waitid(idtype_t idtype,id_t id,siginfo_t *infop,int options);
//成功返回0,失败-1
idtype
:指定了id
类型,可以为下列常量
P_PID
:等待特定进程。此时id
表示要等待的子进程的进程ID
P_GID
:等待属于特定进程组的任一子进程。此时id
表示要等待的进程组ID
P_ALL
:等待任一子进程。此时忽略id
id
:指定的进程id
或者进程组id
infop
:一个缓冲区的地址。该缓冲区由waitid
填写,存放了造成子进程状态改变的有关信号的详细信息options
:指示调用者关心哪些状态变化。可以是下列常量的按位或:
WCONTINUED
:等待这样的子进程:它以前曾被停止过,此后又继续执行,但是其状态尚未报告WEXITED
:等待已经终止的子进程WNOHANG
:如无可用的子进程终止状态,立即返回而不是阻塞WNOWAIT
:不破坏子进程的终止状态,该子进程的终止状态可以由后续的wait,waitid,waitpid
调用取得WSTOPPED
:等待这样的子进程:它已经停止,但是其状态尚未报告wait3/wait4
函数:可以返回终止子进程及其子子进程的资源使用情况
#include
#include
#include
#include
pid_t wait3(int *staloc,int options,struct rusage *rusage);
pid_t wait4(pid_t pid,int *staloc,int options,struct rusage *rusage);
//成功返回子进程ID,失败返回-1
参数:
staloc
:存放子进程终止状态的缓冲区的地址。如果你不关心子进程的终止状态,则可以设它为空指针NULL
rusage
:一个缓冲区的地址,该缓冲区存放由wait3,wait4
返回的终止子进程的资源统计信息,包括:用户CPU时间总量、系统CPU时间总量、缺页次数、接收到的信号的次数等pid
和options
参数与waitpid
相同
就是说父子进程之间会因为共享文件而造成竞争,可采用信号量,管道加以解决
当进程调用一种exec
函数时,该进程执行的程序完全替换成新程序,而新程序则从main
函数开始执行
exec
前后,进程ID
并未改变。因为exec
并不创建新进程exec
只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段有7种不同的exec
函数可以供使用,它们被统称称作exec
函数:
#include
int execl(const char *pathname,const char *arg0,.../*(char *) 0 */);
int execv(const char *pathname,char *const argv[]);
int execle(const char *pathname,const char *arg0,.../*(char *) 0
,char *const envp[] */);
int execve(const char *pathname,char *const argv[],char *const envp[]);
int execlp(const char *filename,const char*arg0,.../*(char *) 0*/);
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd,char *const argv[],char *const evnp[]);
//成功不返回,失败-1
这几个函数的区别:
filename
中包含/
,则视为路径名filename
不包含/
,则按照PATH
环境变量指定的各个目录中搜寻可执行文件execl,execlp,execle
要求将新程序的每个命令行参数都说明为一个单独的参数,这种参数表以空指针结尾;函数execv,execvp,execve,fexecve
应先构造一个指向各参数的指针数组,然后将该指针数组的地址作为参数
l
表示列表list
v
表示矢量vector
l
形式中,必须以空指针结尾,否则新程序根本不知道要读取多少个参数。空指针就是命令行参数序列终止的标记v
形式中,数组的最后一个元素必须是空指针,否则报错。e
结尾的execle,execve,fexecve
可以传递一个指向环境字符串指针数组的指针。注意这个数组的最后一个元素必须是空指针,否则报错。其他四个函数则使用调用进程的environ
变量为新程序复制现有的环境注意:
POSIX
中,这个值至少是 4096 字节exec
之后,新程序的进程ID不变,进程的大多数属性不变。但是对打开文件的处理要注意:
exec
时会关闭该文件描述符;否则该文件描述符仍然保持打开。系统默认行为是不设置执行时关闭标志exec
之后,进程的实际用户 ID 和实际组 ID不变,但是进程的有效用户 ID 要注意:
execve
是内核的系统调用。另外 6 个只是库函数。它们最终都要调用该系统调用在main
函数中调用test_exec
函数
void test_exec()
{
M_TRACE("--------- Begin test_exec() ---------\n");
char buffer[1024];
getcwd(buffer,1024);
char *pathname=abs_path(buffer,"print_arg");
_test_execl(pathname); // 绝对路径名
_test_execv(pathname); // 绝对路径名
_test_execle(pathname); // 绝对路径名
_test_execve(pathname); // 绝对路 径名
_test_execlp("print_arg"); //相对文件名
_test_execvp("print_arg"); //相对文件名
M_TRACE("--------- End test_exec() ---------\n\n");
}
这里调用的print_arg
程序非常简单,就是打印环境变量和命令行参数。其程序如下:
//************* print_arg 程序的源代码 **********//
// //
// 该程序的功能是打印环境变量以及参数列表 //
// //
//********************************************//
#include
#include
#include
extern char **environ;
void print_environ()
{
printf("\t************Environment***************\n");
char **ptr=environ;
while(*ptr!=NULL)
{
printf("\t'%s'",*ptr);
ptr++;
}
printf("\n");
}
int main(int argc, char *argv[])
{
printf("\t************Argument List***************\n");
for(int i=0;i<argc;i++)
{
printf("\t'%s'",argv[i]);
}
printf("\n");
print_environ();
return 0;
}
编译print_arg
程序后,将它放置在目录build-APUE-Desktop_Qt_5_5_1_GCC_64bit-Debug
下,并且向PATH
中添加主目录路径。在shell
中输入命令:
PATH=$PATH:/home/huaxz1986/build-APUE-Desktop_Qt_5_5_1_GCC_64bit-Debug
这是因为execlp
和execvp
需要PATH
环境变量中寻找filename
。如果未添加合适的路径,则程序提示指定的文件不存在。
最终调用结果输出到文件,内容为(省略号中为环境变量内容,因太长所以这里只截取一部分):
--------- Begin test_exec() ---------
--------- End test_exec() ---------
************Argument List***************
'execle_arg1' 'execle_arg2'
************Environment***************
'execle_env1=1' 'execle_env2=2'
************Argument List***************
'execv_arg1' 'execv_arg2'
************Environment***************
'XDG_VTNR=7' 'LC_PAPER=zh_CN.UTF-8' 'LC_ADDRESS=zh_CN.UTF-8'
...
'LC_NAME=zh_CN.UTF-8' 'XAUTHORITY=/home/huaxz1986/.Xauthority' '_=./APUE'
************Argument List***************
'execve_arg1' 'execve_arg2'
************Environment***************
'execve_env1=1' 'execve_env2=2'
************Argument List***************
'execvp_arg1' 'execvp_arg2'
************Environment***************
'XDG_VTNR=7' 'LC_PAPER=zh_CN.UTF-8' 'LC_ADDRESS=zh_CN.UTF-8'
...
'LC_NAME=zh_CN.UTF-8' 'XAUTHORITY=/home/huaxz1986/.Xauthority' '_=./APUE'
************Argument List***************
'execlp_arg1' 'execlp_arg2'
************Environment***************
'XDG_VTNR=7' 'LC_PAPER=zh_CN.UTF-8' 'LC_ADDRESS=zh_CN.UTF-8'
...
'LC_NAME=zh_CN.UTF-8' 'XAUTHORITY=/home/huaxz1986/.Xauthority' '_=./APUE'
************Argument List***************
'execl_arg1' 'execl_arg2'
************Environment***************
'XDG_VTNR=7' 'LC_PAPER=zh_CN.UTF-8' 'LC_ADDRESS=zh_CN.UTF-8'
...
'LC_NAME=zh_CN.UTF-8' 'XAUTHORITY=/home/huaxz1986/.Xauthority' '_=./APUE'
可以发现:
execl/execv/execvp/execlp
继承了父进程的环境变量;execle/execve
指定了环境变量execvp/execlp
在PATH
中正确搜索到了可执行文件execv/execvp/execve
指定的参数表数组必须以空指针结尾,否则exec
失败execle/execve
指定的环境变量数组必须以空指针结尾,否则exec
失败PATH
环境变量包含了一张目录表,称作路径前缀。目录之间用冒号:
分隔。如PATH=/bin:/usr/bin:.
.
表示当前目录:xxx
,在中间,零长前缀为xxx::xxx
,在行尾,零长前缀为xxx:
基本的进程控制原语:
fork
创建进程exec
初始化执行新的程序exit
终止进程wait
等待子进程终止在设计应用程序时,应该使用最小特权模型:程序应当只具有为完成给定认为所需的最小的特权
setuid/setgid
函数:设置实际用户ID和有效用户ID/ 实际组ID和有效组ID
#include
int setuid(uid_t uid);
int setgid(gid_t gid);
uid
:待设置的用户ID
gid
:待设置的组ID
设置的规则为:
setuid
函数将实际用户ID
,有效用户ID
以及保存的设置用户ID
(saved set-user-ID
) 全部设置为uid
(此时uid
没有限制)uid
等于实际用户ID
或者保存的设置用户ID
,则setuid
只会将有效用户ID
设置为uid
,不改变实际用户ID
和保存的设置用户ID
errno
设置为EPERM
并返回 -1_POSIX_SAVED_IDS
为真。如果未提供此功能,则对于保存的设置用户ID
部分都无效setgid
的讨论类似setuid
操作系统内核为每个进程维护3个用户ID
:实际用户ID
、有效用户ID
、保存的设置用户ID
:
ID
ID
是在用户登录时,由login
程序设置的,而且绝不会改变它。login
是一个超级用户进程,当它调用setuid
时,设置所有的3个用户ID
ID
时,exec
函数才设置有效用户ID
。如果程序文件的设置用户ID
位没有设置,则exec
函数不会改变有效用户ID
,而是维持其现有值
setuid
将有效用户ID
设置为实际用户ID
或者保存的设置用户ID
setuid
时,有效用户ID
不能随意取值,只能从实际用户ID
或者保存的设置用户ID
中取得ID
是由exec
复制有效用户ID
而得到。如果设置了程序文件的设置用户ID
位,则exec
根据文件的用户ID
设置了进程的有效用户ID
之后,这个副本就保存起来getuid
获取进程的当前实际用户ID
,可以通过geteuid
获取进程的当前有效用户ID
,但是没有函数获取进程当前的保存的设置用户ID
POSIX
提供了两个函数:
#include
int seteuid(uid_t uid);
int setegid(gid_t gid);
uid
:待设置的有效用户ID
gid
:待设置的有效组ID
seteuid
只修改进程的有效用户ID
;setegid
只修改进程的有效组ID
。
seteuid
将设置进程的有效用户ID
为uid
(此时uid
没有限制)seteuid
只能将进程的有效用户ID
设置为它的实际用户ID
或者保存的设置用户ID
setegid
的讨论类似seteuid
getlogin
:获取运行该程序的用户的登录名
#include
char *getlogin(void);
NULL
通常失败的原因是:进程的用户并没有登录到系统。比如守护进程。
示例:在main
函数中调用test_setuid_seteuid
函数:
void test_setuid_seteuid()
{
M_TRACE("--------- Begin test_setuid_seteuid() ---------\n");
struct passwd* result=My_getpwnam("huaxz1986");
if(NULL==result)
{
M_TRACE("--------- End test_setuid_seteuid() ---------\n\n");
return;
}
My_getlogin();
printf("\n********** Before set id **********\n");
print_uid();
print_gid();
print_euid();
print_egid();
printf("\n********** After set id **********\n");
My_setuid(result->pw_uid); // 二选一
My_setgid(result->pw_gid); // 二选一
My_seteuid(result->pw_uid); // 二选一
My_setegid(result->pw_gid); // 二选一
// My_setuid(0); // 二选一
// My_setgid(0); // 二选一
// My_seteuid(0); // 二选一
// My_setegid(0); // 二选一
print_uid();
print_gid();
print_euid();
print_egid();
M_TRACE("--------- End test_setuid_seteuid() ---------\n\n");
}
我们首先在普通用户状态下,将那些 id
都设置成超级用户所属的用户ID
和组ID
:
然后,我们在超级用户状态下,将那些id
都设置成普通用户的用户ID
和组ID
可以看到:
ID
和有效用户ID
设置为超级用户root
ID
和有效用户ID
为任意值,但是无法修改组ID
和有效组ID
getlogin
都调用失败另外没有给出的截图是:超级进程一旦将自己的用户ID和有效用户ID设置为普通用户之后,该进程退化为普通进程
exec
不仅可以执行二进制可执行文件,也可以执行解释器可执行文件。
解释器可执行文件时文本文件,其首行格式为:
#! /bin/sh
其中/bin/sh
(或者其他路径)通常是绝对路径名,对它不进行任何特殊的处理
实际上exec
不仅可以执行二进制可执行文件,也可以执行解释器可执行文件。
解释器可执行文件时文本文件,其首行格式为:
#! /bin/sh
其中/bin/sh
(或者其他路径)通常是绝对路径名,对它不进行任何特殊的处理
实际上**exec
执行的并不是解释器文件(它是个文本),而是由/bin/sh
指定的二进制可执行文件**,然后/bin/sh
以该解释器文件作为参数
对解释器可执行文件的识别是由操作系统内核来完成的。该识别步骤是作为exec
系统调用处理的一部分来完成的
注意该解释器文件必须要有可执行权限。可以通过chmod a+x
添加任意用户的可执行权限
然后/bin/sh
以该解释器文件作为参数
对解释器可执行文件的识别是由操作系统内核来完成的。该识别步骤是作为exec
系统调用处理的一部分来完成的
注意该解释器文件必须要有可执行权限。可以通过chmod a+x
添加任意用户的可执行权限
system`函数:在程序中执行一个命令字符串
#include
int system(const char *cmdstring);
cmdstring
:命令字符串(在shell
中执行),如 "ps -aux"
system
用于将一个字符作为命令来执行。它等同于同时调用了fork、exec、waitpid
。有三种返回值:
fork
失败或者waitpid
返回除了EINTR
之外的错误,则system
返回 -1,并且设置errno
以指示错误类型exec
失败(表示不能执行shell
),则其返回值如同shell
执行了exit(127)
一样system
返回值是shell
的终止状态,其格式在waitpid
中说明system
对操作系统依赖性很强。目前在UNIX
操作系统上,system
总是可用的。如果cmdstring
为空指针,则如果system
返回 0 表示该操作系统不支持system
函数;否则支持。
system
相较于fork+exec
的优点是:system
进行了所需的各种出错处理以及各种信号处理。缺点是:一旦调用system
的进程具有超级用户权限,则system
执行的命令也具有超级用户权限。
因为
system
的实现过程中并没有更改有效用户ID和实际用户ID的操作。
- 因此如果一个进程以特殊的权限运行,而它又想生成另一个进程执行另外一个程序,则它应该直接使用
fork_exec
并且在fork
之后,exec
之前改回普通权限。- 设置用户
ID
和设置组ID
程序绝不应该调用system
函数
在main
函数中调用test_system
函数:
void test_system()
{
M_TRACE("--------- Begin test_system() ---------\n");
My_system("ls /home"); //命令存在
My_system("ttttt"); // 不存在命令
M_TRACE("--------- End test_system() ---------\n\n");
}
注意:调用system
后不再需要调用wait
等进程控制原语了。这一切控制由system
打包
大多数UNIX
系统提供了一个选项以进行进程会计处理
accton pathname
则会启用会计处理,会计记录会写到pathname
指定的文件中
会计记录结构定义在
头文件中。虽然各个操作系统的实现可能有差别,但是基本数据如下:
typedef u_short comp_t;
struct acct
{
char ac_flag; //标记
char ac_stat; //终止状态
uid_t ac_uid; //真实用户ID
gid_t ac_gid; //真实组ID
dev_t ac_tty; // 控制终端
time_t ac_btime;// 起始的日历时间
comp_t ac_utime;// 用户 CPU 时间
comp_t ac_stime;// 系统 CPU 时间
comp_t ac_etime;// 流逝时间
comp_t ac_mem; // 平均内存使用
comp_t ac_io; // `read`和`write`字节数量
comp_t ac_rw; // `read`和`write`的块数
char ac_comm[8];//命令名。对于LINUX ,则是 ac_comm[17]
};
ac_flag
记录了进程执行期间的某些事件:
AFORK
:进程是由fork
产生的,但从未调用exec
ASU
:进程使用超级用户特区ACORE
:进程转储core
(转储core
的字节并不计算在会计记录内)AXSIG
:进程由一个信号杀死init
进程以及内核守护进程不会产生会计记录A exec B, B exec C
,则只会写一个会计记录。在该记录中的命令名对应于程序C
,但是CPU
时间是程序A,B,C
之和times`函数:任何进程都可以用该函数获取它自己以及已经终止子进程的运行时间
#include
clock_t times(struct tms *buf);
buf
:执行tms
结构的指针。该结构由times
填写并返回一个进程可以度量的有3个时间:
墙上时钟流逝的时间。从进程从开始运行到结束时钟走过的时间,这其中包含了进程在阻塞和等待状态的时间
用户 CPU
时间:用户进程获得了CPU资源以后,在用户态执行的时间
与用户进程对应的是内核进程
系统 CPU
时间:用户进程获得了CPU资源以后,在内核态的执行时间
进程的三种状态为阻塞、就绪、运行
- 墙上时钟流逝的时间 = 阻塞时间 + 就绪时间 +运行时间
- 用户CPU时间 = 运行状态下用户空间的时间
- 系统CPU时间 = 运行状态下系统空间的时间
- 用户CPU时间+系统CPU时间=运行时间
times
函数就是获取进程的这几个时间的。这里的tms
结构定义为:
struct tms{
clock_t tms_utime; //用户 CPU 时间
clock_t tms_stime; //系统 CPU 时间
clock_t tms_cutime; //终止的子进程的用户 CPU 时间的累加值
clock_t tms_cstime; //终止的子进程的系统 CPU 时间的累加值
注意:
times
,然后取两次墙上时钟的差值tms_cutime
和tms_cstime
包含了wait
函数族已经等待到的各个子进程的值clock_t
可以使用_SC_CLK_TCK
(用sysconf
函数)转换成秒数任意进程都可以得到其实际用户ID和有效用户ID及组ID
一个用户ID可以有多个登录名
#include
char *getlogin(void);
成功返回登录名字符串指针,失败返回NULL;
UNIX系统的调度策略和调度优先级是内核确定的。进程可以通过调整nice
值选择以更低优先级运行
POSIX
实时扩展增加了进一步细调的行为nice
值的在UBUNTU 16.04
中范围是 -20~19 之间。
nice
值越小,优先级越高(该进程抢占能力更强,更霸道);nice
值越大,优先级越低(从而该进程是“友好的”)nice
值nice
函数:进程通过它来获取自己的nice
值或者修改自己的nice
值:
#include
int nice(int incr);
incr
:nice
值的增量nice
值incr
会被增加到调用进程的nice
值上。
incr
值太大,系统会直接将它降到最大合法值,不会出错(UBUNTU 16.04
中是 19)incr
值太小,系统会直接将它提高到最小合法值,不会出错(UBUNTU 16.04
中是 -20)
nice
返回 -1 时,需要综合errno
才能判断是否出错getpriority/setpriority
函数:获取/设置进程的nice
值:
#include
int getpriority(int which,id_t who);
int setpriority(int which,id_t who,int value);
参数:
which
:控制who
参数是如何解释的。可以取三个值之一:
PRIO_PROCESS
:表示进程PRIO_PGRP
:表示进程组PRIO_USER
表示用户ID
who
:选择感兴趣的一个或者多个进程。
who
为0,which
为PRIO_PROCESS
,返回当前进程的nice
值who
为0,which
为PRIO_PGRP
,则返回进程组中最小的nice
值who
为0,which
为PRIO_USER
,则返回调用进程的实际用户ID
拥有的那些进程中最小的nice
值value
:nice
的增量返回值:
getpriority
:成功返回-20~19
之间的nice
值;失败返回 -1getpriority
不仅可以获得本进程的nice
值,还可以获取一组相关进程的nice
值。而setpriority
可以为本进程、进程组、属于特定用户ID
的所有进程设置优先级
示例:在main
函数中调用test_getpriority_setpriority
函数:
void test_getpriority_setpriority()
{
M_TRACE("--------- Begin test_getpriority_setpriority() ---------\n");
create_child();
// 只有父进程能到此处
check_waitpid();
My_getpriority(PRIO_PROCESS,0); // 父进程自己的 nice 值
M_TRACE("--------- End test_getpriority_setpriority() --------\n\n");
}
可以看到,如果为普通用户,则没有权限降低nice
值。因为普通进程没有权限提升其优先级(即降低nice
值)。在超级用户权限下,结果如下:
在main
函数中调用test_progress_times
函数:
void test_progress_times()
{
M_TRACE("--------- Begin test_progress_times() ---------\n");
assert(prepare_file("test","abc",3,S_IRWXU)==0);
int fd=My_open("test",O_RDWR);
if(-1==fd)
{
un_prepare_file("test");
M_TRACE("--------- End test_fork() ---------\n\n");
return;
}
//****** 打开文件成功 *************//
clock_t t1,t2;
struct tms buf;
t1=times(&buf);
create_child(fd,1000000000);// 子进程直接 _exit
create_child(fd,2000000000);// 子进程直接 _exit
sleep(5);// 让子进程获得锁,否则父进程持有锁,然后等待子进程结束,最后死锁
fcntl_lock(fd); // 加锁
busy_work(1000000000);// 只有父进程能到达这里
check_waitpid();
t2=My_times(&buf);
printf("Parent elapsed time is %d s\n",clock_2_second(t2-t1));\
fcntl_unlock(fd); // 解锁
close(fd);
un_prepare_file("test");
M_TRACE("--------- End test_progress_times() ---------\n\n");
}
该示例的父进程派生了两个子进程。每个子进程都睡眠了 2秒
busy_work
实现的)。如果没有busy_work
,则进程的用户CPU时间为0wait
子进程才能收集子进程的信息。否则父进程的cstime
和cutime
均为0可以看到:
utime
和stime
wait
user cpu time
等于279+559=838
个时钟滴答,约等于 839
个时钟滴答。等于 8 秒system cpu time
等于0+6
个时钟滴答,等于0 秒。user cpu time
(等于2秒)之外,还加上睡眠时间( 2 秒)等于4秒user cpu time
(等于5秒)之外,还加上睡眠时间( 2 秒)等于7秒user cpu time
),一共是 13秒。但是注意到:子进程一的user cpu time
为 279个时钟滴答, 子进程二的user cpu time
为 559 个时钟滴答,父进程的user cpu time
为 280个时钟滴答。这三个时间加在一起应该是 1118 个时钟滴答,约 11秒。而我们前面计算是,为 2+5+2=9秒。因此父进程运行时间为 13秒+ 2秒=15秒父进程
sleep
时,正好子进程在运行。由于记录锁的存在,时间可以这样累加。
BSD系统:
当系统自举时,内核创建进程ID为 1 的进程,即init
进程
init
进程读取文件/etc/ttys
,对每个允许登录的终端设备,init
调用一次fork
,其所生成的子进程则exec getty
程序(以一个空的环境)
getty
对终端设备调用open
函数,以读、写方式将终端打开。
一旦设备被打开则文件描述符0、1、2被设置到该设备
然后getty
输出login:
之类的信息,并等待用户键入用户名
当用户键入了用户名后,getty
的工作就完成了,它以类似下列的方式调用login
程序:
execle("/bin/login","login","-p",username,(char *)0,envp);
其中envp
是getty
以终端名和在gettytab
中说明的环境字符串为login
创建的环境。-p
标志通知login
保留传递给它的环境,也可以将其他环境字符串添加到该环境中,但是不能替换它
login
能处理多项工作
login
得到了用户名,所以能够调用getpwnam
获取相应用户的口令文件登录项,然后调用getpass
以显示Password:
login
读取用户键入的口令,它调用crypt
将用户键入的口令加密,并且与该用户在阴影口令文件中登录的pw_passwd
字段比较login
以参数1调用exit
表示登录过程失败
init
了解了子进程的终止情况后,再次调用fork
其后又调用了getty
,对此终端重复上述过程login
完成下列工作:
chown
更改该终端的所有权,使得登录用户成为它的所有者setgid
和initgroups
设置进程组IDlogin
得到的所有信息初始化环境:起始目录(HOME
)、shell
(SHELL
)、用户名(USER
和LOGNAME
)以及一个系统默认路径(PATH
)login
进程调用setuid
,将进程的用户ID更改登录用户的用户ID
,并调用该用户的登录shell
,其方式类似于:execl("/bin/sh","-sh",(char*)0);
shell
开始运行。登录shell
读取其启动文件(如.profile
)。这些启动文件通常是更改某些环境变量并增加很多环境变量。当执行完启动文件后,用户最后得到shell
提示符,并能键入命令MAC OS X
系统:它部分地给予Free BSD
,因此启动步骤与FreeBSD
几乎相同,除了:
init
工作是由launchd
完成的Linux
:步骤几乎与Free BSD
相同。但是init
读取的是/etc/inittab
文件而不是/etc/ttys
文件
网络登录:对于网络登录,所有登录都是经由内核的网络接口驱动程序。
BSD
系统中,由init
执行shell
脚本/etc/rc
,此shell
脚本启动inetd
守护进程。由inetd
负责处理网络登录Linux
系统中,使用xinetd
代替inetd
进程进程组:每个进程除了有一个进程ID之外,还属于一个进程组。进程组是一个或者多个进程的集合。
pid_t
数据类型中getpgrp/getpgid
函数:获取进程所属的进程组:
#include
pid_t getpgrp(void);
pid_t getpgid(pid_t pid);
对于getpgrp
函数:其返回值是调用进程的进程组ID
(没有失败值)
对于getpgid
函数:
pid
为待查看进程的进程ID。如果pid=0
,则返回调用进程的进程组ID
这里没有要求
pid
和本进程的关系
ID
;失败返回 -1setpgid
函数:加入一个现有的进程组或者创建一个新进程组
#include
int setpgid(pid_t pid,pid_t pgid);
pid
:待处理的进程的进程ID
pgid
:进程组的组ID
setpgid
函数将pid
进程的进程组ID
设置为pgid
pid
等于pgid
,则由pid
指定的进程变成进程组组长pid
等于0,则使用调用者的进程ID
pgid
等于0,则使用pid
指定的进程ID
用作进程组ID
注意:一个进程只能为它自己或者他的子进程设置进程组ID,且进程组ID只能为父进程进程组ID、父进程的进程ID或者子进程的进程ID。
exec
之后,它就不再更改子进程的进程组IDshell
中,fork
之后立即调用此函数,使得父进程设置其子进程的进程组ID,同时也使子进程设置其自己的进程组ID(这两个调用是冗余的,但是是个双保险)会话session
是一个或者多个进程组的集合。
setsid
函数:创建一个新会话
#include
pid_t setsid(void);
进程调用setsid
建立一个新会话。如果调用此函数的进程不是一个进程组的组长进程,则此函数创建一个新会话并且发生下面三件事:
该进程会变成新会话的会话首进程session leader
。此时该进程是新会话中的唯一进程
会话首进程是创建该会话的进程
该进程成为一个新进程组的组长进程。新进程组ID就是该调用进程的进程ID
该进程没有控制终端。即使调用setsid
之前该进程有一个控制终端,该联系也被切断
如果调用此函数的进程是个进程组的组长,则此函数返回出错。
通常是进程首先
fork
,然后父进程终止,子进程调用setsid
继续执行。这确保了子进程不是一个进程组的组长
getsid
函数:返回进程所在的会话ID
(会话ID
等于会话首进程的进程组ID,会话首进程总是进程组的组长进程,因此它也等于会话首进程的进程ID)
#include
pid_t getsid(pid_t pid);
pid
:待查看进程的进程ID
ID
如果pid
为0,则getsid
返回调用进程的会话ID。如果pid
并不属于调用者所在的会话,则调用进程就不能得到该会话ID
在main
函数中调用test_getsid_setsid
函数:
void test_getsid_setsid()
{
M_TRACE("--------- Begin test_getsid_setsid() ---------\n");
create_child();
// 只有父进程能到达此处
check_waitpid();
print_pid();
print_parent_pid();
My_getpgid(0);
My_getsid(0);
My_setsid();
My_getsid(0);
M_TRACE("--------- End test_getsid_setsid() ---------\n\n");
}
会话和进程组还有一些特性:
controlling terminal
controlling process
Ctrl+C
),都会将中断信号发送至前台进程组的所有进程Ctrl+\
),都会将退出信号发送至前台进程组的所有进程tcgetpgrp/tcsetpgrp
函数:获取/设置当前进程所在会话的前台进程组ID
#include
pid_t tcgetpgrp(int fd);
int tcsetpgrp(int fd,pid_t pgrpid);
参数:
fd
:进程在fd
这个描述符上打开的终端pgrpid
:待设置的前台进程组ID返回值:
tcgetpgrp
:成功则返回前台进程组ID,失败返回 -1tcsetpgrp
:成功返回 0;失败返回 -1如果进程有一个控制终端,则该进程可以调用tcsetpgrp
将前台进程组ID设置为pgrpid
,其中:
pgrpid
必须是同一个会话的一个进程组的IDfd
必须引用该会话的控制终端注意:大多数应用程序并不直接使用这两个函数,它们通常是由作业控制shell
调用
tcgetsid
函数:获取会话首进程的进程组ID(也就是会话ID)
#include
pid_t tcgetsid(int fd);
fd
:进程在fd
这个描述符上打开的终端注意会话ID不一定等于前台进程组的组ID。对于一个会话,会话ID通常不变(前提是没有函数主动设置它);但是前台进程组进程由于作业调度会经常发生变化
运行在一个终端上启动多个作业,它控制哪个作业可以访问终端以及那些作业在后台运行
Linux
中,当执行ls > a.out &
等命令以&
时,就启动了一个后台作业
shell
会赋予它一个作业标识符,并打印作业标识符以及一个或者多个进程IDLinux
中,当执行ls > a.out
等命令时,就启动了一个前台作业Ctrl+C
中断字符:产生SIGINT
信号Ctrl+\
退出字符:产生SIGQUIT
信号Ctrl+Z
挂起字符:产生SIGTSTP
信号SIGTTIN
,该信号通常会停止此后台作业,而shell
会向用户发出这种情况的通知
shell
命令将该后台作业转换为前台作业运行stty
命令可以禁止或者允许后台作业输出到终端。
SGITTOU
信号,该信号通常会停止此后台作业,而shell
会向用户发出这种情况的通知shell
命令将该后台作业转换为前台作业运行shell如何执行一个进程,以及这与进程组、控制终端、和会话概念的关系。
一个进程组不是孤儿进程组的条件是:该进程组中存在一个进程,其父进程在属于同一个会话的另一个组中
如果一个进程组中的所有进程:
- 要么其父进程不再同一个会话中
- 要么其父进程就在同一个组中
则该进程组是个孤儿进程组
当孤儿进程组产生的时候,如果孤儿进程组中有TASK_STOP
的进程,那么就发送SIGHUP
和SIGCONT
信号给这个进程组
这个顺序是不能变的。我们知道进程在进程在TASK_STOP
的时候是不能响应信号的,只有当进程继续运行的时候,才能响应之前的信号。
SIGCONT
信号再发送SIGHUP
信号,那么SIGCONT
信号后,进程就开始重新进入运行态,这个和马上响应SIGHUP
信号的用意相悖TASK_STOP
的过程中首先发送SIGHUP
信号,为的是让进程运行之后马上执行SIGHUP
信号。这两个信号是发送给有处于TASK_STOP
状态的进程的进程组的所有进程的。所以进程组中正在运行的进程,如果没有建立SIGHUP
信号处理函数,那么运行的进程就会因为SIGHUP
退出。
这一届梳理了各种结构体之间的相互作用
本章主要就是说明了进程之间的关系——会话
信号是软中断,它提供了一种处理异步事件的方法
每个信号都有一个名字,这些名字都以SIG
开头:
中POSIX
将 0 号编号值称作空信号Mac OS X 10.6.8
以及Linux 3.2.0
都支持31种信号很多条件可以产生信号:
当用户按某些终端键时,引发终端产生信号。如当用户在终端上按Delete
键(通常是Ctrl+C
)时,产生中断信号SIGINT
硬件异常信号:除数为0、无效的内存引用等等
这些条件通常由硬件检测到,并通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。如对执行一个无效内存引用的进程产生
SIGSEGV
信号
进程调用kill()
函数可将任意信号发送给另一个进程或者进程组
要求接收信号的进程和发送信号的进程的所有者必须相同,或者发送信号的进程的所有者是超级用户
用户可以用kill
命令将任意信号发送给其他进程
此命令只是
kill()
函数的接口。通常用于终止一个失控的后台进程
当检测到某种软件条件已经发生并应将其通知有关进程时,也产生信号。如定时器超时的时候产生SIGALRM
信号
进程可以告诉内核当某个信号发生时,执行下列三种操作之一(我们称作信号处理):
SIGKILL
和SIGSTOP
信号决不能被忽略
SIGKILL
和SIGSTOP
向内核和超级用户提供了使进程终止或者停止的可靠方法SIGKILL
和SIGSTOP
信号SIGCANCEL
(线程库内部使用)、SIGCHILD
(子进程状态改变)进程执行时,如果没有显式设定,则所有的信号的处理都为默认动作
fork
之后,子进程继承父进程的信号处理方式,因为子进程在开始时复制了父进程的进程空间exec
函数会将原先设置为要捕捉的信号都改为默认动作,非捕捉的信号则不变。这是因为信号捕捉函数的地址很可能在新程序中没有任何意义unix系统信号机制最简单的接口是signal函数,
#inclide<signal.h>
void(*signal(int signo, void (*func)(int))) (int);
//成功则返回信号处置配置,出错返回SIG_ERR
参数:
早期信号机制存在各种问题
如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就会被中断而不再继续执行。此时该系统调用返回出错,其errno
设置为EINTR
为了支持中断的系统调用,我们将系统调用分成两类:低速系统调用和非低速系统调用
pause
函数(根据定义,它使调用进程休眠直到捕捉一个信号)和wait
函数ioctl
函数为了帮助应用程序使其不必处理被中断的系统调用(即不需要人工来重新启动被中断的系统调用),4.2BSD
引进了某些被中断的系统调用自动重启动
ioctl、read、readv、write、writev、wait、waitpid
4.3BSD
运行进程基于每个信号禁用重启动功能read、write
系统调用就要进行是否出错返回的测试;如果是被中断的,则需要再调用read、write
系统调用POSIX
要求:只有中断信号的SA_RESTART
标志有效时,才重启动被该信号中断的系统调用
进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断
CPU 首先执行该信号处理程序中的指令
如果从信号处理程序返回,则继续执行进程正在执行的正常指令序列
有可能无法从信号处理程序返回,如在信号处理程序中调用
_exit()
或者longjmp
但是有个问题:在信号处理程序中,无法判断捕捉到信号的时候,进程执行到何处。
malloc
,那么在信号处理程序中,绝不应该再调用malloc
。否则会破坏malloc
维护的存储区链表对于某一类函数,如果在捕捉到信号的时候,进程正在执行这些函数,那么在信号处理程序中,可以安全的重复调用这些函数。这一类函数称作可重入函数
SUS
规范说明了在信号处理程序中保证调用安全的函数。这些函数有以下特点:
malloc
或者free
。调用malloc
或者free
的函数不是可重入的IO
函数。使用标准IO
函数的函数不是可重入的。因为标准IO
库很多都是用了全局数据结构当在信号处理函数中调用可重入函数时,应当在调用前保存errno
,然后在调用后恢复errno
errno
值。而这种改变并不属于进程的正常执行逻辑。