Bootstrap

Linux编程---进程基础

进程这个概念大家都很熟悉了吧...我就不多说了..

 

首先是进程环境.也就是Shell相关的内容.

这都是很基础的我就挑一些我自己都不太清楚的写写.

.命令行参数

POSIX对命令行的语法约定:

1.实用程序名至少两个,至多9个字符,且只包含小写字母和数字.(Linux应该不止9个吧.我觉得这是Unixshell的规定).

2.选项名必须是单个字母或者数字,-W选项保留给实现扩展使用,不允许多数字选项.

3.选项和它的选项值可以作为也可以不作为分开的单词.-ofoo-o foo是一样的..(我看的达内视频,里面有个老师就喜欢这么写..)

4.如果多个选项均不要求有选项值的话,可以集中在一个短横线分隔符之后作为一个单词,因此’-abc’和’-a -b -c’等价.(貌似鸟哥书上写过...但是我完全忘了...)

5.第一次出现的参数’--’终止所有选项.后继的任何参数均视为操作数.即使他们是以’-’开头的单个字符也是如此.貌似安装程序的时候遇到这个情况比较多...但我记得我一搞写好多个’--’接在后面.

6.多个选项可按任意顺序出现,单个选项也可多次出现,其解释留给应用程序.

7.仅由一个短横线字符组成的单词按操作数解释.UNIX系统约定,它用于指定来自标准输入流的输入或标准输出流的输出.例如,如下命令中,单个短横线’-’是选项参数-o的值,他代表标准输出流,即指定将会变文件输出到标准输出流.

gcc  -S  file.c  -o  - -pipe

这个意思我猜是直接把输出输出到NULL文件中...-pipe表示用管道代替过程中的临时文件.我也是第一次晓得这个-pipe参数.这么写了之后比较简单的makefile貌似都可以不写clean?

 

是否每个shell程序都要写一个函数来分析这些命令行参数呢?

实际上已经有了库函数帮忙做这件事情了...(原来写过几个小程序都是自己写函数分析的....)

int getopt(int argc,char 8const argv[],const char *optstring);

前两个参数就是main后面的参数.第三个参数就是解析出来串的结果.每次调用一个getopt则获得一个optstring参数.

这个optstring参数可能比较复杂.注意,这里不是提取出参数放到optstring,而是根据optstring来提取出参数是怎样的..还是举个例子吧.

c=getopt(argc,argv,”:f:is”)

“:f:is”中的顺序不重要.开头的”:”表示在后面遇到错误之后不返回’?’,而是返回’:’.

”f:”表示f这个参数后面要跟一个操作数.

第一次调用optstring的时候会对optstring分析.然后每次遇到f,i,s这三个之一的参数时,就会返回其当得到参数类型(f,i,s之一).返回f的时候,同时会设置optarg.这个optarg是一个全局字符指针.也就是相当于把’:’在命令行中所表示的操作数给optarg.当遇到未知参数的时候,optarg被设置为’?’.同时c也被设置为’?’.

这个函数配置了四个全局变量

optarg----就是解析出来的内容

optind----再次调用getopt的时候要分析argv.这是个下标值.

optopt----最后一个未知选项.

opterr----用来报错.如果在getopt调用之前就设置不为0,并且opstring的开头不是”:”,getopt在遇到位置选项字符或缺少选项所要求的值时,会自动打印一个错误消息到标准错误流中.设置成0的话,将不打印任何错误信息,但仍然返回”?”或”:”指明错误.

 

我感觉已经解释的差不多了...写完介绍突然觉得好麻烦...还不如自己写函数分析.....

 

二.环境变量

shell标准环境变量.

HOME 用户的主目录或初始默认目录

LANG 对于地区设置的预定义的名字

LC_COLLATE 字符排序的地区类别名

LC_CTYPE 字符处理的地区类别名

LC_MONETARY 包含有关货币数字编辑信息的地区类别名

LC_NUMERIC 包含数字编辑信息(例如基字字符)的地区类别名

LC_TIME 日期/时间格式信息的地区类别名

LOGNAME 与当前进程相联系的注册名

PATH 可执行文件的路径前缀

TERM 终端类型

TZ 时区信息

 

每个进程的环境表

extern char ** environ;这是进程的环境变量表指针.

其内容有HOME,PATH,SHELL,USER,LOGNAME这几个环境变量.

 

访问环境变量

char * getenv(const char *name);

int putenv(char *str);

第一个函数参数就是环境变量的名字.返回环境变量串指针.没什么好说的.

第二个函数则是增加一个环境变量或者去掉一个环境变量的定义.如果参数是name=value的形式.那么就会增加一个环境变量.如果是name已存在,那么就会把旧的定义去掉.同时设置为成新的值.并且不要以局部变量作为字符数组作为str的参数.

 

补充一点,实际上环境变量在进程虚拟地址空间中是在内核区中.这一点我是看APUE和百度的一张图结合起来发现的.当然也有可能是在用户栈栈底,不过我感觉应该是在内核中.

 

三.终止进程

只学过语言的估计觉得在main里面return就足够了吧...但实际上多线程之后return在很多情况下就不合适了.就要用下面的函数了

int exit(int status);

可能有的人还见过_exit.实际上exit就是调用_exit来退出并且返回到内核的.但是实际上在返回到内核之前,还要执行一些清理工作.具体如下:

--atexit()注册时相反的顺序调用所有注册函数.

--关闭所有打开的流.

--删除用tmpfile()建立的所有临时文件.

--调用exit(status)终止进程.

 

这里exitstatus可以用两个宏来定义.EXIT_SUCCESSEXIT_FAILURE.其实就是01.

 

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

表示注册一个函数,到进程正常终止时调用其注册的函数.简单来说相当于进程的析构函数.

我猜对于pthread应该是用atexit注册了一个函数.所以在进程终止的时候就可以关闭线程了.并且查了才知道atexit最多只能注册32个函数.但线程是很多的.估计只能这样了.还有pthread_cleanup_pushpop好像就没有上限限制.应该都是针对线程的注册函数知道的一个变量来操作的吧.

貌似这里可以搞一个恶作剧~可以注册一个函数,这个函数里面通过exec并且把路径传递进去~那么这个进程就永远关不掉了~~~~~如果我要是黑客...我就会搞一下这个~用钩子把原先的位置记录下来,调用一个atexit,然后再把PC寄存器的值还回去~~~当然先得我学会用钩子这种东西再说吧~~~

 

流产函数

void abort();

这个以执行立即终止程序.并且不用atexit注册的清理函数.但在终止程序之前关闭所有打开的流并生成一个core文件.

所有异常终止程序的原因都是由信号造成的.实际上abort就是通过生成一个SIGABRT信号来流产的.

 

 

四.进程的存储空间.

这个基本都是C的库函数,我自己补充一下sbrkbrk的好了.之前看达内视频的时候就说准备总结的.

#include<malloc.h>

void * memalign(size_t boundary,size_t size);

void * valloc(size_t size);

这两个函数我也没见过...资料也好少,不过既然是包含在malloc.h中的.应该不是C库里面的函数吧.malloc的功能基本差不多.但提供了一种灵活控制所分配存储边界对齐要求的能力.memalignboundary表示其返回的地址应当是boundary的倍数,并且必须是2的幂..同时size表示分配内存的大小.,单位为字节.

我书上的参数顺序和网上搜出来的不一致.具体的顺序还是man一下比较好.

 

valloc分配一块大小为size并对齐在页边界的存储空间.相当于是

void * valloc(size_t size){
return memalign(getpagesize(),size);

}

 

实际上这两个函数分配的内存通常比malloc的大,实际上就是用malloc分配后,然后在把分配的一部设置一下,.再把一个对齐的地址返回来.所以最后的地址并不是malloc分配的地址.在有些系统上不能用free来释放其空间.但在Linux下可以用free来释放掉.

 

实际上malloc分配的也会比实际多分配一些空间.然后在分配空间的最后记录一些分配信息.所以对malloc分配的空间写时,别超过其指定大小.如果写超了,那么很可能会破坏其malloc的数据结构,导致malloc无法再分配空间.

 

堆的管理

int brk(void *endds);

char *sbrk(int inor);

brk为系统调用,sbrkC的库函数.用法上两个都是一样的功能,即设置堆的边界.他们malloc的底层实现.

brk简单来说就是把堆的空间向上扩张或向下收缩.并且根据参数来设置.一般用一个基址+偏移的方式来传值.

sbrk针对的也是堆空间.

sbrk(0)可以精确的返回当前虚拟内存的使用情况,也就是堆的边界地址.所以可以通过brk(sbrk(0)+偏移)来扩张或收缩堆得大小.这里偏移如果是正数就是增加堆空间,负数就减少堆空间.

百科上面还提到coreleft函数可以看剩余未分配的空间.具体我就不说了.

 

 

五.setjmplongjmp

这两个函数应该对于学C比较深入的早都看过了吧.原来看书的时候感觉蛮神奇的.但现在一想应该就是那样吧.看来实力提升了不少啊~

int setjmp(jmp_buf env);

void longjmp(jmp_buf env,int val);

setjmp就是设置一个返回点.就像游戏中的存档一样.调用了setjmp就相当于存了个档.再用longjmp的时候就相当于是读档了.

setjmp保存当前的栈环境等数据.然后返回0.其环境的值也全部保存到env中了.如果从longjmp返回的话,则是返回一个非零值.longjmp的第二个参数就是其setjmp的第二次返回的返回值.

 

对于错误返回的时候,这两个函数比较有用.毕竟C没有try{}catch,有了这个就不用从深层函数调用中一层一层返回了.提高了处理错误的效率.

 

实际上就是根据栈的结构来模拟,并且记录位置.具体我就不说了,学了汇编自然会明白.

 

六.进程资源

int getrlimit (int resource,struct rlimit *rlptr);

int setrlimit (int resource,const struct rlimit *rlptr);

看函数名字就很清楚功能了.

 

其中struct rlimit包含两个变量.

rlim_t rlim_cur;    当前值

rlim_t rlim_max;   限制值

 

resource参数有以下一些值:

RLIMIT_AS 虚拟地址空间的最大字节大小.

RLIMIT_CORE   core文件的最大字节大小.0表示不创建core文件

RLIMIT_CPU    进程能够使用的最大CPU时间数.用秒为单位.超过时间限制的话,系统发送SIGXCPU信号到进程.

RLIMIT_DATA   进程数据段空间的最大值..data.bss和堆空间之和.

RLIMIT_FSIZE   进程能够创建的最大文件的字节数.超过其限制,系统发送SIGXFSZ信号

RLIMIT_MEMLOCK  表示可以用mlockmlockall锁住在物理存储器中的最大虚拟存储字节数.

RLIMIT_NOFILE  进程最多能打开的文件数.超过得到错误码EMFILE

RLIMIT_NPROC   每个用户实际能创建子进程的最大数.超过则会让fork返回ENGAIN.

RLIMIT_RSS   表示一个进程最大可以被分配的页面数.如果物理内存足够,那么可能会超过这个值.

RLIMIT_STACK  栈的最大字节数.如果其图扩充栈,超过这个值,则会得到SIGSEGV信号.

RLIMIT_NLIMITS  任何合法的resource对应资源的值必须小于这个数.相当于总闸.

RLIMIT_INFINITY  表示对指定资源没有限制.infinity就是无限的意思

 

 

资源使用统计函数

int getrussage(int who,struct rusage *rusage);

who指定是自己还是子进程.有宏RUSAGE_SELF(调用此函数的进程)RUSAGE_CHILDREN(调用此函数的子进程)

struct rusage的结构比较复杂包含

struct timeval ru_utime;  表示执行用户指令所花的时间

struct timeval ru_stime;  表示操作系统代表进程所花的时间

其中struct timeval包含

time_t tv_sec;     表示秒数

suseconds tv_usec; 微秒数

 

rusage还有一些其他成员.比如驻内存最大存储空间大小.进程被且切换出内存的次数.读和写硬盘的次数,收到信号的次数,进程间通信量统计,页缺失率,甚至还包含各种存储段的使用大小.等信息.用到的话再去man...

 

七.用户信息

1.用户名

char *getlogin();

得到用户的注册名.返回的是函数内部的一个静态地址.所以换了个进程之后再调用这个函数可能会把原来的值给覆盖掉.

当用户退出,但进程还在时.getlogin返回NULL,并且设errnoENXIO.

2.用户数据库

主要是/etc/passwd中的数据结构.其影子文件为/etc/shadow

到的数据结构struct passwd.成员如下:

char *pw_name 用户名

char *pw_passwd 加密的口令

uid_t pw_uid 用户ID

gid_T pw_gid 组ID

char *pw_gecos 用户的全名以及其他个人信息

char *pw_dir 初始工作目录

char *pw_shell 初始shell或用户注册时运行的初始程序

使用函数如下

struct passwd * getpwuid (uid_t uid);

struct passwd * getwnam(const char *name);

这两个函数返回的都是函数内的静态数据.getlogin一样内容会被下一次调用所覆盖.

这两个函数都是一次全部返回特定用户的登记项.

 

下面的则是一次一个用户地读所有用户的登记项.

struct passwd *getpwent();

void setpwent();

void endpwent();

getpwent返回指向passwd数据结构的指针.第一次调用它时,该结构含有口令数据库文件的第一个登记项.再调用时,这个结构被更新乘下一个登记项.

setpwentendpwent分别打开和关闭用户数据库.也就是说setpwen可以让其指针指向第一个数据项.

 

3.组数据库

根据组文件/etc/group和组口令影子文件/etc/gshadow.

系统用struct group来表示其数据结构

char *gr_name 组名

char *gr_passwd 加密后的口令

int gr_gid 组ID

char **gr_mem 指向组的各个成员的指针数组,NULL结尾.

和用户数据库的函数一样.也有两种获得结构信息的方法.

struct group *getgrgid(gid_t gid);

struct group *getgrnam(const char *name);

struct group *getgrent();

void setgrent();

void endgrent();

用法和上面用户的一模一样,我就不多说了.

并且对于一个用户来说可以属于多个组.其中第一个组是基本组.而后面的组则是附加组.

 

一个进程可以调用getgroups函数获取它的附加组ID

int getgroups(int gidsetsize,gid_t grouplist[]);

第一个参数指明grouplist的元素存放个数.并且返回值返回已经存入grouplist中的元素个数.如果gidsetsize指定为0,那么返回进程附加组的个数.但是不修改grouplist中的数据.

 

 

八.进程身份凭证

由于有些程序是属于共享的.但是其程序过程中又需要其他用户的权限.那么在我执行这个程序的时候,就可能遇到权限不足的情况.所以用户ID和组ID实际上是不够的.那么就得增加新的机制来保证共享程序对于非属主也是可用的.

实际用户  实际组ID     指明实际用户是谁,即创建进程的实际用户

有效用户ID 有效组ID  附加组ID  用于操作和文件访问权限测试,当前起作用的ID.一般有效用户ID和实际用户ID是一样的.如果某个文件设置了调整用户/ID位指明该文件是要调整用户ID或调整组ID方式来执行的话,exec文件加载该文件时,会将有效用户ID或有效组ID设置成文件的拥有者的用户ID或者拥有者的组ID.简单来说,如果你的程序只有你能用,但又想给外人用,那么可以通过函数设置有效ID.

保存的调整用户ID  保存的调整组ID   exec保存,从有效用户ID和有效组ID复制的.exec()在用执行文件的实际ID或调整用户ID设置进程的有效ID之前,先保存进程的当前有效ID到保存的调整用户ID.这样一来,保存的调整用户ID和保存的调整组ID使得程序能够恢复它在exec调用之前建立的有效用户ID和有效组ID.


这和文件中定义的权限很像.我就不多说了..

下面是用于获得这些文件权力参数的函数

uid_t getuid();

uid_t geteuid();

gid_t getgid();

gid_t getegid();

uid是实际用户id,euid是有效用户id.同理gidegid一样.

 

调整进程的身份

int setuid(uid_t uid);

int segid(gid_t gid);

int setreuid(uid_t ruid,uid_t euid);

int setregid(gid_t rgid,gid_t egid);

int seteuid(uid_t uid);

int setegid(gid_t gid);

这几个函数意思都很明确了.就是第三个和第四个函数是uideuid一起设置的.如果填上-1

则表示不设置.

只有超级用户才能让进程设置ruideuid为任意值.普通用户进程只能改变有效用户ID回到实际用户ID或保存调整用户ID.ruid必须等于实际用户ID或有效用户ID或者保存的调整用户ID.

 

 

实际用户ID

有效用户ID

保存的调整用户ID

exec

调整用户ID位清除

-

-

从有效用户ID复制

调整用户ID位设置

-

设置为程序文件的用户ID

从有效用户ID赋值

setuid

超级用户

设置为uid

设置为uid

设置为uid

普通用户

-

设置为uid,uid必须为实际用户ID或保存的调整用户ID

-

setreuid

超级用户

设置为ruid

设置为euid

 

普通用户

设置为ruid,ruid必须为有效用户id

设置为euid,euid必须为实际用户id或保存的调整用户id

 

seteuid

超级用户

-

设置为uid

-

普通用户

-

设置为uid,uid必须为实际用户id或保存的调整用户id

-

‘-’表示无改变.

 

获得进程当前有效用户名ID对应的用户名

char *cuserid(char *string);

返回与调用进程有效用户ID相连的用户名.string返回.大小应当是L_cuserid(这个宏感觉很奇怪...一下大写一下小写的...百度了下发现只有这种....)另外其字符串也可以用返回值返回.不过这个地址是函数的静态变量.下次调用就会改变地址.

 

这个函数和getlogin很像.不过这个得到的是进程文件的属主名.getlogin则是进程执行者的名字.

 

九.进程控制

1.标识.

进程是由PID来唯一确定的.(如果不涉及到线程的话).

pid_t getpid();   得到进程的PID

pid_t getppid();  得到进程的父进程PID

 

2.进程的创建

pid_t fork();

一次调用,两次返回.对于子进程返回为0.父进程返回为子进程的PID.

子进程基本是照搬的父进程地址空间.

 

如果子进程创建后立马调用exec.那么就用vfork.

pid_t vfork();

这个函数类似于fork.只是在创建的时候复制的内容相对少一些.fork复制数据段和堆栈段.vfork直接共享数据段.这样创建的代价就少了很多,就便于快速的创建一个进程.并且这个函数还有一个好处就是会阻塞父进程,直到子进程执行了execexit函数.

 

这里还有clone函数,一般是用来创建线程.

int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

这个函数可以选择性的共享段.我在线程里面写了,这里就不多说了.

  

3.执行一个进程

这有6个函数,我就写常用的3.

int execv(const char *path,const char 8argv[]);

int execvp(const char *file,const char 8argv[]);

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

path参数必须绝对路径.file参数则是根据PATH环境变量给出的目录中查找.

最后一个函数还附上了环境变量.

一般都跟着forkvfork一起用.所以很多东西都没变.只是把新程序装进来罢了.

不过要注意几点:

--文件描述符仍然保持.除了设置了FD_CLOEXEC的文件除外.可以在exec之前用fcntl设置

--目录流会关闭.

--信号句柄设置在原进程中为忽略(SIG_IGN的仍然忽略.有句柄的设置为(SIG_DFL).并且信号栈且SA_ONSTACK标志被清除.

--清空atexit注册的函数

--如果打开的文件的文件系统有ST_NOSVID.那么各类ID均不变.否则,如果新进程的调整用户ID方式位被设置,则新进程的有效ID被设置为文件属主的ID.类似组也相同.

(这个感觉,正常开发没人会用多个文件系统吧,需要再man一下ST_NOSVID)

 

4.等待进程完成

pid_t wait(int *stat_loc);

pid_t waitpid(pid_t pid,int *stat_loc , int options);

两个函数是检查子进程是否终止.并且一直阻塞等待到子进程终止,然后回收.stat_loc参数则是其子进程的返回值.

对于第二个函数的pid,实际上表示方式比较多

<-1  等待进程组ID等于pid绝对值的任意子进程

-1   等待任意子进程,等价于wait.

0    等待进程组ID与调用进程的进程组ID相同的任意子进程.

>0  等待进程IDpid的进程终止.

并且options还有多个选项.

WNOHANG 表示非阻塞查询,没结果的话就返回0

WUNTRACED 表示如果系统支持作业控制,pid可以指定任何子进程.如果已经停止并且自从停止之后还未报告其状态,则必须报告其状态.

 

父进程如果先于子进程退出,那么子进程则会曾为init进程的子进程.

如果子进程先于父进程死亡,那么子进程就会成为僵死进程.

如果父进程没有调用wait来等待.那么子进程的proc结构一直不能释放并且子进程保持僵死状态知道程序终止甚至直到系统重启.

对于现代UNIX系统允许进程不等待它的子进程,进程可以通过sigaction函数指明SA_NOCLDWAIT标志来指定SIGCHLD信号的动作.这可使内核在调用者的子进程终止时不创建僵死进程.

 

5.system函数

int system(const char *command);

这个参数就是shell语句.这个不用多说...

内部过程就是通过fork,execwaitpid来实现的.

不能启动shell,返回127,其他错误返回-1并设置errno.调用成功返回shell终止的状态.


6.进程组

实际上shell可以通过括号,分号分隔的方式,一次执行多条shell命令.这些命令就会成为一个进程组.每个进程组有一个组长.PID等于其PGID进程组ID.并且所有其他进程都是组长的子孙.

pid_t getpgrp();

int setpgid(pid_t pid,pid_t pgid);

第一个是得到组ID.

第二个则是设置自己的进程组ID.也可以设置其子孙的.但子孙必须是没有调用exec函数的.

如果参数pidpgid都是调用进程的pid,那么这个进程则会成为一个新进程组.

并且这里的pid不能是会晤期主席并且必须与会晤期主席属于同一个会晤期.

 

7.会晤期

会晤期是一到多个进程的集合(集合为什么要叫"期"......).一般把一次注册产生的所有进程属于同一个会晤期.

每个会晤期都有一个会晤期主席.是创建会晤期的那个进程.

可以通过下面函数来创建一个会晤期

pid_t setsid();

---调用进程成为会晤期主席

---调用进程成为了一个新进程组的组长

---调用进程没有控制终端,有的话也要先断开

通常,新的会晤期是由系统的注册程序创建的,并且会晤期主席就是运行用户注册shell的进程.因此,会晤期主席是在一个会晤期期间所创建的所有进程的祖先.负责派生并执行用户注册shell的程序安排该注册shell在它自己的会晤期和进程组内运行.从创建会晤期所隐含的上述三点知道,会晤期主席也是进程组组长.不过,应当了解进程组组长并不一定是会晤期主席.当会晤期主席终止时,他结束会晤期.

 

其实我也没看懂这个是干什么的..感觉就是setpgid的两个参数相同的情况....

 

8.控制终端

每个会晤期可以有一个控制终端,这通常就是用户注册时所在的终端(用户交互界面?),因此也称为注册终端.进程可以通过控制终端进行输入,输出和控制作业的运行.

简单来说就是给进程配一个tty.

char *ctermid(char *ptr);

返回终端的文件名字字符串.可以通过返回值指针,也可以是ptr返回.但都是函数内的静态变量.再调用一次就被改写了.

 

 

设置终端

pid_t tcgetpgrp(int fd);

int tcsetpgrp(int fd,pid_t pgrpid);

第一个函数fd是终端对应的文件描述字.函数返回与该终端前台进程组的进程组ID.如果没fd相连的终端没有前台进程组,tcgetpgrp返回一个大于1的数.此数不同于任何现存进程组的进程组ID.

第二个函数则是设置前台终端为pgrpid进程组所有.调用进程必须是与pgrpid同一个会晤期的进程,fd引用的必须是会晤期控制终端.成功返回0.

这两个函数都仅在系统宏_POSIX_JOB_CONTROL有定义时才有定义.,否则均错误返回.

 

 

就写到这了吧...后面终端什么的.感觉完全可以写个shell脚本来做这些工作.没必要把程序搞这么复杂....虽然我还是写了.....

;