一 进程和程序区别
程序:存储在磁盘上可执行指令的集合,是一个文件.典型格式elf
进程:程序的一次执行过程,伴随资源的分配和释放
进程执行的时候需要的资源:内存,时间片,CPU ,文件
二 Linux 进程
(1)进程数据组成:
| 程序 | 系统 |
代码段,rodata段,data段 ,bss段,堆和栈,一组寄存器的值
linux系统下用struct task_struct 结构体描述一个进程(也有称PCB进程控制块)。
进程表项即struct task_struct *指针,指向一个对应的进程
(2)进程的标识
PID : 进程本身的标识,内核用于区分不同进程 getpid(2)
PPID : 进程的父进程标识 getppid(2)
用户通过进程名也可区分不同进程
不同进程可能进程名相同,PID一定不同
查看进程的PID,PPID
ps -ef | grep 进程名
(3)进程的分类
a.交互进程 b.批处理进程 c.守护进程(在后台运行,不随终端设备的关闭而结束执行)
*************四 进程相关命令*********************/*{{{*/
(1)ps
a.查看PID,PPID
例如:ps -ef | grep 进程名/进程ID
b.查看进程的状态
ps aux | grep 进程名/进程ID
R:运行或就绪状态 D:不可中断的等待态 S:可中断的等待态 T:停止态 Z:僵尸态(进程结束了,其父进程没有收尸处理)
+表示进程在前台进程组中
(2)kill
功能:给指定PID进程发信号
常用的用法:
(3)killall
功能:给指定同名的进程发信号
常用的用法信号指定与kill类似:
killall -信号 进程名
(4)nice
功能:运行程序的时候,间接指定进程的优先级
默认进程运行时优先级PR为20
可能过top命令查看进程的NI(nice值)
其中PR和 NI 值,都会影响进程执行的优先级:
NI是Nice值,PR是优先级,Nice值是进程的一个属性,PR是根据Nice排序的
Priority/PR 由 OS 内核动态调整,用户不能调整(PR 值越低,进程执行的优先级越高)
Nice/NI 用户可以自己调整
相互关系:有个一般公式,PR(new) = PR(old) + NI
但是,PR 是 OS 动态调整的,但是 PR 的最终值还需要由 OS 分析决定的(虽然 NI 会影响 PR)
nice [-20,19],nice值越小,会使进程的优先级越高
例如:运行程序的时候,指定nice值
nice -18 ./a.out
指定一个负的nice ,此时会使进程优先级提高,需要root权限
sudo nice --15 ./a.out
(5)renice
功能:改变正在运行的进程优先级
如果是将进程优先级升高需要root权限,即加sudo执行
renice nice值 PID
例如:
renice 10 PID
sudo renice -10 PID
(6)bg
将挂起的进程放到后台运行
执行一个耗时程序
$./a.out
^Z # ctrl + z向进程发信号SIGTSTP使交互进程被挂起(暂停)
[1]+ Stopped ./a.out
$bg 1 #根据[jobs]号将对应任务放到后台运行
[1]+ ./a.out &
(7)fg
将后台运行的进程放到前台运行
后台运行一个程序
$./a.out &
[1] 18855 #显示后台进程的[jobs号]和PID
$fg 1 #根据jobs号将对应任务放到前台运行
./a.out
**********************************/*}}}*/
五 创建子进程
pid_t fork(void)
功能:创建子进程
返回值: 失败返回-1;给父进程返回子进程PID,给子进程返回0
创建子进程的过程:拷贝父亲的堆,栈,rodata段,data段,bss段,一组寄存器的值,其中代码段共享
注意:
1.fork之后父,子进程谁先执行是不确定,取决系统的调度算法
2.fork之后,父子进程都是从fork下一条语句开始执行
3.fork时,父进程的正文段和子进程共享,数据段,堆栈段,堆,打开的文件等资源均让子进程拷贝一份
4.fork之后,父子进程各自拥用独立4G地址空间,相互并不影响
5.fork后子进程继承父进程打开的文件,与父进程使用相同的文件表项,对一个文件共用offset值
父子进程打开文件的方式
1.fork前打开文件,父子进程共用相同的文件表项
2.fork后父子进程各自独立打开同一文件,各自拥用指向同一文件的文件表项,
(当打开方式中有O_TRUNC时,后打开文件的进程会将先打开进程写入数据清0)
3.fork前打开文件,父子进程共用相同的文件表项,
子进程先关闭文件,再重新打开一次同一文件,打开时不用O_TRUNC,不会产生第2种方式的问题
1)练习:
1.创建一个子进程,让子进程打印其PID,然后死循环
父进程打印PID,然后死循环
2.用ps命令查看父子进程的PID,PPID
3.杀死子进程,用ps命令查看父子进程的状态
4.杀死父进程,用ps命令查看父子进程的状态
#include <stdio.h> #include <unistd.h> int main( int argc, const char *argv[]) { pid_t pid; if ((pid = fork()) == -1) { perror("fork" ); return -1; } if (pid > 0) { printf("father pid = %d ppid = %d\n" ,getpid(),getppid()); } else { printf("child pid = %d ppid = %d\n" ,getpid(),getppid()); } printf("pid = %d ppid = %d\n" ,getpid(),getppid()); return 0; }
2)练习:
1.在fork之前用文件IO打开文件
2.创建子进程
3.父进程循环从键盘输入数据写入到文件,子进程循环从文件中读取数据打印,如果父进程输入"quit",父子进程结束
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> int write_file( int fd) { char buf[100] = {0}; while (1) { fgets(buf,sizeof (buf),stdin); write(fd,buf,strlen(buf)); lseek(fd,-strlen(buf),SEEK_CUR); if (strncmp(buf, "quit" ,4) == 0 ) break ; } return 0; } int read_file( int fd) { char buf[100] = {0}; int n = 0; while (1) { n = read(fd,buf,sizeof (buf)-1); buf[n-1] = '\0' ; if (n == 0) { continue ; } printf("%s:%s\n" ,__FUNCTION__,buf); if (strncmp(buf, "quit" ,4) == 0 ) break ; } return 0; } int main( int argc, const char *argv[]) { pid_t pid; int fd ; char buf[10]; int n = 0; if (argc != 2) { fprintf(stderr,"Usage : %s file\n" ,argv[0]); return -1; } if ((fd = open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666)) < 0) { perror("open" ); return -1; } if ((pid = fork()) == -1) { perror("fork" ); return -1; } if (pid > 0) { printf("father:\n" ); write_file(fd); } if (pid == 0) { printf("child:\n" ); read_file(fd); } return 0; }
vfork和fork的区别:fork函数创建的子进程后父进程与子进程的调度执行顺序不确定,
而vfork创建的子进程先运行,父进程会阻塞,直到子进程结束或调用exec函数;
子进程结束前和父进程共用相同物理页
六 exec函数族
功能:在一个进程中载入执行另外程序
过程:exec用执行的程序,替换原进程的数据段,代码段,堆栈段,只保留原进程的PID
int execl( const char *path, const char *arg, ...); int execl(可执行所在的路径,可执行文件名,参数1,参数2,..,NULL); execl("/bin/ls" , "ls" , "-l" ,NULL); int execlp( const char *file, const char *arg, ...); int execlp(可执行文件名,可执行文件名,参数1,参数2,..,NULL); execlp("ls" , "ls" , "-l" ,NULL); char *p_arr[] = {可执行文件名,参数1,参数2,...,NULL}; int execv( const char *path, char * const argv[]); int execv(可执行文件路径,指针数组名); int execl(可执行所在的路径,可执行文件名,参数1,参数2,..,NULL); execl("./a.out" , "a.out" , "src.c" , "dest.c" ,NULL); char *p_arr[] = { "a.out" , "src.c" , "dest.c" ,NULL}; execv("./a.out" ,p_arr);
练习:执行"ls -l"
/*通过execv()来执行拷贝文件*/
#include <stdio.h> #include <unistd.h> int main( int argc, const char *argv[]) { execl("/bin/ls" , "ls" , "-l" ,NULL); return 0; }
v and p
int execvp( const char *file, char * const argv[]); int execvp(可执行文件名,指针数组名); char *p_arr[] = { "ls" , "-l" , "." ,NULL}; execvp("ls" ,p_arr); execvp(p_arr[0],p_arr); char *strtok( char *str, const char *delim); str: delim: str: NULL (接着上一次操作的字符串,向后分割) delim:分割符串
返回值:返回分割后子串的首地址
注意:
分割到最后遇到'\0'时,返回最后一个子串的首地址,如果下一次再进行分割,则返回 NULL
printenv PATH echo $PAHT getenv - get an environment variable #include <stdlib.h> char *getenv( const char *name); char *p = getenv( "PWD" ); execle #include <unistd.h> extern char **environ; int execl( const char *path, const char *arg, ...); int execl int execle( const char *path, const char *arg,..., char * const envp[]); int execle char *p_env[] = { "PATH=./" , "USER_NAME=tim" ,NULL}; execle("./myenv" , "myenv" ,NULL,p_env); execve(2) #include <unistd.h> int execve( const char *filename, char * const argv[], char * const envp[]); int execve
exec函数簇的各函数区别
1.除execve(2)是系统调用外,其它函数均为库函数
2.可执行文件查找方式 p:PATH环境变量提供路径前缀
3.参数表传递方式 l:list列举 v:vector 指针数组
4.环境变量的使用 e:environ 通过指针数组指定新程序的执行环境
作业:
父子进程拷贝文件,父进程拷贝前一半,子进程拷贝后一半
#include <head.h> int get_size_src( int fd_src) { int len; len = lseek(fd_src,0,SEEK_END); return len; } int main( int argc, const char *argv[]) { int fd_src,fd_dest; fd_src = open(argv[0],O_RDONLY); if (argc < 3){ fprintf(stderr,"Usage : %s file1 file2\n" ,argv[0]); exit(EXIT_FAILURE); } if (fd_src < 0){ perror("Fail to open : " ); exit(EXIT_FAILURE); } fd_dest = open(argv[2],O_WRONLY | O_CREAT | O_TRUNC,0666); if (fd_dest < 0){ perror("Fail to open : " ); exit(EXIT_FAILURE); } int len; len = get_size_src(fd_src); int pid = fork(); if (pid < 0){ perror("Fail to fork : " ); exit(EXIT_FAILURE); } char buf[1024]; int n = 0; if (pid == 0){ close(fd_src); close(fd_dest); fd_src = open(argv[1],O_RDONLY); if (fd_src < 0){ perror("Fail to open : " ); exit(EXIT_FAILURE); } fd_dest = open(argv[2],O_WRONLY); if (fd_dest < 0){ perror("Fail to open : " ); exit(EXIT_FAILURE); } lseek(fd_src,len / 2,SEEK_SET); lseek(fd_dest,len / 2,SEEK_SET); n = read(fd_src,buf,len / 2); write(fd_dest,buf,n); close(fd_src); close(fd_dest); }else { lseek(fd_src,0,SEEK_SET); lseek(fd_dest,0,SEEK_SET); n = read(fd_src,buf,len / 2); write(fd_dest,buf,n); } return 0; }
1.获得文件大小?stat,lseek
2.先产生空洞扩充目标文件大小.ftruncate truncate或lseek + write
3.文件打开方式,
4.父子进程是独立打开文件还是fork后继承?
父子各自打开同一文件方式1 fd = open(,O_WDONLY | O_TRUNC | O_CREAT,0666); pid = fork(); if (pid == 0) { close(fd); fd = open(,O_WDONLY); } 父子各自打开同一文件方式2 pid = fork(); if (pid > 0) { fd = open(,O_WDONLY | O_TRUNC | O_CREAT,0666); write(); } if (pid == 0) { fd = open(,O_WDONLY | O_TRUNC | O_CREAT,0666); write(); }
四、 return,exit,_exit
exit(3) exit - cause normal process termination #include <stdlib.h> void exit( int status); The C standard specifies two constants, EXIT_SUCCESS and EXIT_FAILURE, that may be passed to exit() to indicate successful or unsuccessful termination, respectively. _exit(2) _exit, _Exit - terminate the calling process #include <unistd.h> void _exit( int status); The function _exit() terminates the calling process "immediately" . Any open file descriptors belonging to the process are closed; any children of the process are inherited by process 1, init, and the process's parent is sent a SIGCHLD signal.
return:结束一个函数的调用,主要用函数返回
exit ,_exit :结束一个进程的运行
exit,_exit区别:exit函数结束进程时候,会刷新缓存,而_exit直接结束进程,不刷新缓存
五、 wait 和 waitpid (1)wait pid_t wait(int *status); 功能:回收处于僵尸态的子进程,如果没有僵尸态的子进程则阻塞,如果没有子进程会立即返回 参数: status 获得子进程退出的状态,如果不想获得子进程状态,wait参数status可以为NULL 以下status为int status = 0;变量的值 WIFEXITED(status) returns true if the child terminated normally, that is, by calling exit(3) or _exit(2), or by returning from main(). WEXITSTATUS(status) returns the exit status of the child. This consists of the least significant 8 bits of the status argu‐ ment that the child specified in a call to exit(3) or _exit(2) or as the argument for a return statement in main(). This macro should only be employed if WIFEXITED returned true. WIFSIGNALED(status) returns true if the child process was terminated by a signal. WTERMSIG(status) returns the number of the signal that caused the child process to terminate. This macro should only be employed if WIFSIGNALED returned true. 返回值:成功返回子进程的PID, 失败返回-1 例如: 回收僵尸态进程资源,并获得子进程退出状态 int status; wait(&status); 仅收尸,不关心子进程退出状态 wait(NULL); 练习:fork一个子进程,子进程打印自己的pid,然后死循环,(用信号终止子进程) 父进程wait子进程结束,要获得子进程终止的信号编号
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> int main( int argc, const char *argv[]) { int status; pid_t pid; if ( ( pid = fork() ) == -1) { perror("fork" ); return -1; } if ( pid > 0 ) { sleep(3); wait(&status); printf("status = %d\n" ,status); if (WIFEXITED(status)) { printf("child died %d\n" ,WEXITSTATUS(status)); } if (WIFSIGNALED(status)) { printf("sig number = %d\n" ,WTERMSIG(status)); } sleep(2); printf("----------" ); } if ( pid == 0 ) { while (1) { printf("child pid = %d ppid = %d\n" ,getpid(),getppid()); } } return 0; }
(2)waitpid
pid_t waitpid(pid_t pid, int *status, int options);
功能:按指定方式(是否阻塞)探测子进程状态的改变,R->Z,R->T,T->R
<1>options : 下面的宏通过 | 连接
0 options中未指定WNOHANG,以阻塞方式调用,默认可探测子进程终止即从运行态到死亡态R->Z
WNOHANG waitpid以非阻塞调用 如果没有子进程状态发生改变,waitpid不阻塞,立即返回,此时返回值为0
WUNTRACED 子进程状态的改变R->T
WCONTINUED 子进程状态的改变T->R
<2>pid
pid指定为一个特定子进程PID : 只关心指定的子进程状态是否发生改变
pid指定为-1 : 关心所有的子进程
pid指定为0 : 关心和父进程同组的子进程
pid指定为-pid : 关心的是组ID等于|-pid|,组中任意的子进程
注意:
waitpid默认以阻塞方式等待子进程状态变化,options中不加WNOHANG
但waitpid也可以非阻塞调用,此时options需要有 WNOHANG,
即如果没有子进程状态发生改变,waitpid不阻塞,立即返回,此时返回值为0
例:与wait(&status);实现相同功能 R->Z
waitpid(-1,&status,0);
以非阻塞方式等待进程号为pid1的子进程
waitpid(pid1,&status,WNOHANG);
以非阻塞方式等待与父进程同组进程下述三种状态变化 R->Z R->T T->R
waitpid(0,&status,WNOHANG | WUNTRACED | WCONTINUED);
练习:非阻塞调用waitpid()
思考:父进程不阻塞,不轮询,回收僵尸态进程资源
思路1:父进程产生子进程,子进程产生孙进程,让子结束由父收尸,孙进程成为孤儿进程,被init进程收养,
由孙进程完成相应任务,最终init进程给孙进程收尸
阻塞方式:父进程产生子进程,wait(&status)或waitpid(-1,&status,0);等待子进程结束
轮询方式:父进程创建子进程,while(waitpid(-1,&status,WNOHANG) == 0);
六、 进程的UID:
RUID :实际用户ID 进程的创建者
EUID : 有效用户ID 决定进程的访问权限
SUID : 保存设置ID 保存EUID
默认Linux 用户创建的进程:RUID == EUID == SUID
注意:如果一个可执行的程序文件,其set-id-bit被打开,此时创建的进程,其EUID等于文件的所有者
普通用户运行程序,创建进程拥有超级权限
1.将可执行文件所有者变成root
sudo chown root 可执行文件
2.打开可执行文件的set-id-bit位
sudo chmod u+s 可执行文件名
七、 Linux守护进程
Linux下的守护进程daemon
守护进程有三个最基本的特点:后台运行,独立于终端,完成一定的任务。
1.首先所谓的后台运行过程是一般是在图形界面或是终端不可见的;
2.而独立于终端是说它不和终端联系,运行之后一般不接受终端的输入也不向终端输出;
3.每一个守护进程的运行都是为了完成一定的任务而运行的,这些任务一般都是系统相关的任务。
除开这些特殊性以外,守护进程与普通进程基本上没有什么区别
因此,实际上编写守护进程,可以把一个普通进程按照上述的守护进程的特性改造成为守护进程
守护进程必须与其运行前的环境隔离开来。
这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩码等。
这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。
首先要说几个概念,进程组,会话和控制终端
进程组:每运行一个程序或是命令就会产生一个进程组,而每一个进程组有一个组长进程.
进程组由进程组号(GID)标识,进程组号(GID)为组长进程PID,一般进程组的第一个进程是组长进程.
组长进程fork的进程也属于同一个进程组,但是子进程一旦执行exec等函数就会不属于该进程组。
会话:一次登录形成一个会话,一个会话可包含多个进程组(前台或后台), 但只能有一个前台进程组.
setsid(2)可建立一个新的会话,注意进程组的组长进程不能调用,调用进程是新会话的首进程(session leader)
控制终端:会话的首进程(session leader)打开一个终端之后, 该终端就成为该会话的控制终端
与控制终端建立连接的会话首进程称为控制进程,一个会话只能有一个控制终端
在控制终端上产生的输入和信号将发送给会话的前台进程组中的所有进程
终端上的连接断开时 (比如网络断开或 Modem 断开), 挂起信号将发送到控制进程(session leader)
编程实现守护进程也就是要实现上面的三个特点:
编写守护进程步骤:
(1)创建子进程父进程退出
(2)子进程创建新会话期(setsid(),调用者不能是组长进程)
(3)改变进程工作目录为"/",(chdir("/"))
(4)重设文件掩码(umask(0))
(5)关闭不需要的文件描述符
pid = fork(); if (pid > 0) { exit(); } if (setsid() < 0) { } chdir("/" ); umask(0); n = getdtablesize(); for (fd = 0;fd < n;fd++) { close(fd); }
练习:守护进程写时间日志
1.先编写普通进程,
2.可通过命令行传参决定是否成为守护进程
./a.out time_log.txt ON/OFF (0/1)
./a.out time_log.txt 0/1
#include <stdio.h> #include <errno.h>//errno #include <string.h> //strerror #include <time.h> #define N 100 int get_line( FILE *fp) { char buf[N]={0}; int line = 0; while (fgets(buf, sizeof (buf),fp) != NULL){ if (buf[strlen(buf) - 1] == '\n' ) line++; } return line; } void printf_log( FILE *fp) { int line = 0; time_t tm ; struct tm *ptm; line = get_line(fp); while (1){ time(&tm ); ptm = localtime(&tm ); fprintf(fp,"%-4d, %04d-%02d-%02d %02d:%02d:%02d\n" , line++,ptm->tm_year+1900,ptm->tm_mon+1, ptm->tm_mday,ptm->tm_hour,ptm->tm_min,ptm->tm_sec); fflush(fp); fprintf(stdout,"%-4d, %04d-%02d-%02d %02d:%02d:%02d\n" , line,ptm->tm_year+1900,ptm->tm_mon+1, ptm->tm_mday,ptm->tm_hour,ptm->tm_min,ptm->tm_sec); sleep(1); } return ; } int main( int argc, const char *argv[]) { FILE *fp = NULL; int n = 0; pid_t pid; if (argc != 2){ fprintf(stderr,"Usage: %s log\n" ,argv[0]); return -1; } if ((fp = fopen(argv[1], "a+" )) == NULL){ fprintf(stderr,"fopen:%s\n" ,strerror(errno)); return -1; } #if 0 n = get_line(fp); printf("n = %d\n" ,n); #endif if ((pid = fork()) < 0){ perror("error ! \n" ); return -1; } if (pid > 0){ exit(0); }else { if (setsid() == -1){ perror("error ! \n" ); return -1; } chdir("\n" ); umask(0); n = getdtablesize(); for (fp = 0;fp < n;fp ++){ close(fp); } } printf_log(fp); return 0; }
3.守护进程的出错信息通过系统日志文件/var/log/syslog反映
#define LOG(...) {char _bf[1024];snprintf(_bf,sizeof(_bf),__VA_ARGS__);\ fprintf(stderr,"%s" ,_bf);syslog(LOG_ERR, "%s" ,_bf);} fprintf(stderr,"Fail to open %s : %s.\n" ,argv[1],strerror(errno)); LOG("Fail to open %s : %s.\n" ,argv[1],strerror(errno));