018 UNIX再学习 -- 用户 ID 和组 ID
程序员文章站
2022-07-13 12:09:15
...
用户 ID和组 ID 的内容已经在好几章中出现过了。之前都没有讲到,现在放到一起总结。
一、用户 ID 和 组 ID 回顾
1、我们在APUE 第 4、6、8 章,都有涉及到。
其中我们用到的地方:
(1)修改文件权限
chgrp命令 功能:改变文件或目录的属组。
chown 命令 功能:更改某个文件或目录的属主和属组。
-
# ls -l
-
总用量 16
-
-rwxr-xr-x 1 root root 7308 Apr 27 11:28 a.out
-
-rw-r--r-- 1 root root 422 Apr 27 11:28 test.c
第一个 root 为属主,第二个 root 为属组。当然这里面没有用户 ID,和 组 ID
下面解析一下格式所表示的意思。这种表示方法一共有十位:
-
9 8 7 6 5 4 3 2 1 0
-
- r w x r - x r - x
第9位表示文件类型,可以为p、d、l、s、c、b和-:p表示命名管道文件
d表示目录文件
l表示符号连接文件
-表示普通文件
s表示socket文件
c表示字符设备文件
b表示块设备文件
第8-6位、5-3位、2-0位分别表示文件所有者的权限,同组用户的权限,其他用户的权限,其形式为rwx:
r表示可读,可以读出文件的内容
w表示可写,可以修改文件的内容
x表示可执行,可运行这个程序
没有权限的位置用-表示
如果一个文件被设置了 SUID 或 SGID 位,会分别表现在所有者或同组用户的权限的可执行位上。例如:
-rwsr-xr-x 表示SUID和所有者权限中可执行位被设置
-rwSr--r-- 表示SUID被设置,但所有者权限中可执行位没有被设置
-rwxr-sr-x 表示SGID和同组用户权限中可执行位被设置
-rw-r-Sr-- 表示SGID被设置,但同组用户权限中可执行位没有被设置
其实在UNIX的实现中,文件权限用12个二进制位表示,如果该位置上的值是
表示有相应的权限:
-
11 10 9 8 7 6 5 4 3 2 1 0
-
S G T r w x r w x r w x
第11位为SUID位,第10位为SGID位,第9位为sticky位,第8-0位对应于上面的三组rwx位。上面的-rwsr-xr-x的值为: 1 0 0 1 1 1 1 0 1 1 0 1
-rw-r-Sr--的值为: 0 1 0 1 1 0 1 0 0 1 0 0
(2)/etc/passwd
-
# cat /etc/passwd
-
root:x:0:0:root:/root:/bin/bash
-
tarena:x:1000:1000:tarena,,,:/home/tarena:/bin/bash
其中超级用户 root 的用户 ID 为 0,组 ID 也为 0。而我所用的普通用户 tarena 用户 ID 为 1000,组 ID 为 1000。(3)函数 stat
stat 结构体中的 st_uid、st_gid 即用户 ID 和组 ID
-
struct stat {
-
dev_t st_dev; /* ID of device containing file */
-
ino_t st_ino; /* inode number */
-
mode_t st_mode; /* protection */
-
nlink_t st_nlink; /* number of hard links */
-
uid_t st_uid; /* user ID of owner */
-
gid_t st_gid; /* group ID of owner */
-
dev_t st_rdev; /* device ID (if special file) */
-
off_t st_size; /* total size, in bytes */
-
blksize_t st_blksize; /* blocksize for file system I/O */
-
blkcnt_t st_blocks; /* number of 512B blocks allocated */
-
time_t st_atime; /* time of last access */
-
time_t st_mtime; /* time of last modification */
-
time_t st_ctime; /* time of last status change */
-
};
(4)口令文件相关函数
函数 getpwuid 和 getpwnam
函数 getpwent、setpwent 和 endpwent
函数 getpwent、setpwent 和 endpwent
可用于查看 用户 ID 和 组 ID
(5)ps 指令部分
其中各列含义如下:
USER 用户名
USER 用户名
UID 用户ID(User ID)
-
# ps lax
-
F UID PID PPID PRI NI VSZ RSS WCHAN STAT TTY TIME COMMAND
-
4 0 1 0 20 0 3612 2064 poll_s Ss ? 0:01 /sbin/init
-
1 0 2 0 20 0 0 0 kthrea S ? 0:00 [kthreadd]
-
1 0 3 2 20 0 0 0 run_ks S ? 0:01 [ksoftirqd/0]
-
5 0 5 2 20 0 0 0 worker S ? 0:01 [kworker/u:0]
二、获取调用进程用户 ID 和 组 ID
当一个用户通过合法的用户名和口令登录系统以后,系统就会为它启动一个 shell 进程,shell 进程的实际用户 ID 和实际组 ID 就是该登录用户的用户 ID 和组 ID。该用户在 shell 下启动的任何进程都是 shell 进程的子进程,自然也就继承了 shell 进程的实际用户 ID 和实际组 ID。
1、获取调用进程的实际用户 ID 和实际组 ID
-
#include <unistd.h>
-
#include <sys/types.h>
-
uid_t getuid(void);
-
gid_t getgid(void);
-
分别返回调用进程的实际用户ID 和实际组 ID
(1)示例说明
-
#include <stdio.h>
-
#include <unistd.h>
-
#include <sys/types.h>
-
-
int main (void)
-
{
-
printf ("uid = %d\n", getuid ());
-
printf ("gid = %d\n", getgid ());
-
return 0;
-
}
-
输出结果:
-
uid = 0
-
gid = 0
-
-
我用的是超级用户 root 用户ID为 0,组 ID 为 0
一个进程的用户和组身份决定了它可以访问哪些资源,比如读、写或者执行某个文件。但真正被用于权限验证的并不是进程的实际用户 ID和实际组 ID,而是其有效用户ID 和有效组 ID。一般情况下,进程的有效用户 ID和有效组 ID 就取自其实际用户 ID 和实际组 ID,二者是等价的。
2、获取调用进程的有效用户 ID 和有效组 ID
-
#include <unistd.h>
-
#include <sys/types.h>
-
uid_t geteuid(void);
-
gid_t getegid(void);
-
分别返回调用进程的有效用户ID 和有效组 ID
(1)示例说明
-
#include <stdio.h>
-
#include <unistd.h>
-
#include <sys/types.h>
-
-
int main (void)
-
{
-
printf ("euid = %d\n", geteuid ());
-
printf ("egid = %d\n", getegid ());
-
return 0;
-
}
-
输出结果:
-
euid = 0
-
egid = 0
三、设置用户 ID 位和设置组 ID 位
如果用于启动进程的可执行文件带有设置用户 ID 位(SUID)和(或)设置组 ID 位(SGID),那么该进程的有效用户 ID 和(或)有效组 ID 就不再取自其实际用户 ID 和(或)实际组 ID,而是取自可执行文件的拥有者用户 ID 和(或)组 ID。
1、什么是设置用户 ID 位 和设置组 ID 位
例如:
-
# ls -la test
-
-rwsrwsr-x 1 root root 7314 Apr 27 16:10 test
其中第一个 s 为设置用户 ID 位(SUID),第二个 s 为设置组 ID 位(SGID)
设置方法:
第一种:采取字符设置
sudo chmod u+s test 设置SUID位 sudo chmod g+s test 设置SGID位 sudo chmod u-s test 去掉SUID设置 sudo chmod g-s test 去掉SGID设置
第二种:采用数字方式设置:
在一般文件权限设置的3位数字前再加上一位数字,成为4位数字。
如果该位为4, 则表示设置 setuid
如果该位为2, 则表示设置 setgid
所以,假设文件或目录的原来权限位设置是777,要加上SUID/SGID,如下设置:
如果该位为4, 则表示设置 setuid
如果该位为2, 则表示设置 setgid
所以,假设文件或目录的原来权限位设置是777,要加上SUID/SGID,如下设置:
-
chmod 4777 test 设置 SUID 位
-
chmod 2777 test 设置 SGID 位
-
chmod 0777 test 去掉 SUID 设置
-
chmod 0777 test 去掉 SGID 设置
第三种:使用 chmod 函数
再回顾 stat 函数,设置用户 ID 位及设置组 ID 位都包含在文件的 st_mode 值中。这两位可分别用常量 S_ISUID 和 S_ISGID 测试。
-
S_ISUID 0004000 set UID bit
-
S_ISGID 0002000 set-group-ID bit (see below)
例如,设置权限 0644:S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
而设置了 SUID 和 SGID 位。即:S_ISUID | S_ISGID | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
举个例子:
-
#include <stdio.h>
-
#include <sys/stat.h>
-
#include <sys/types.h>
-
#include <fcntl.h>
-
#include <unistd.h>
-
#include <stdlib.h>
-
-
int main (void)
-
{
-
int res = chmod ("a.txt", S_ISUID | S_ISGID | S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
-
if (-1 == res)
-
perror ("fail to chmod"), exit (1);
-
-
execlp ("stat", "stat", "a.txt", NULL);
-
-
return 0;
-
}
-
输出结果:
-
文件:"a.txt"
-
大小:0 块:0 IO 块:4096 普通空文件
-
设备:801h/2049d Inode:2107549 硬链接:1
-
权限:(6644/-rwSr-Sr--) Uid:( 0/ root) Gid:( 0/ root)
-
最近访问:2017-04-27 18:35:15.952010539 +0800
-
最近更改:2017-04-27 18:35:15.952010539 +0800
-
最近改动:2017-04-28 09:26:11.755554531 +0800
-
创建时间:-
而清空使用:~S_ISUID & ~S_ISGID & S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
-
#include <stdio.h>
-
#include <sys/stat.h>
-
#include <sys/types.h>
-
#include <fcntl.h>
-
#include <unistd.h>
-
#include <stdlib.h>
-
-
int main (void)
-
{
-
int res = chmod ("a.txt", ~S_ISUID & ~S_ISGID & S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
-
if (-1 == res)
-
perror ("fail to chmod"), exit (1);
-
-
execlp ("stat", "stat", "a.txt", NULL);
-
-
return 0;
-
}
-
输出结果:
-
文件:"a.txt"
-
大小:0 块:0 IO 块:4096 普通空文件
-
设备:801h/2049d Inode:2107549 硬链接:1
-
权限:(0644/-rw-r--r--) Uid:( 0/ root) Gid:( 0/ root)
-
最近访问:2017-04-27 18:35:15.952010539 +0800
-
最近更改:2017-04-27 18:35:15.952010539 +0800
-
最近改动:2017-04-28 09:27:11.747554284 +0800
-
创建时间:-
这个结果是 6644/-rwSr-Sr--, 其中 s 和 S 还是有区别的,上面提到 S 表示 SUID/SGID 被设置,但所有者权限中可执行位没有被设置。因为设置 SUID/SGID 需要有运行权限。如果简单的用 chmod 函数来设置好像还是不行。所以不推荐使用第三种方法。
找出当前设置了 suid 的文件方法:
参看:C语言再学习 -- Linux下find命令用法
-
# find . -perm -4000 -type f
-
./a.txt
2、SUID 和 SGID 位 作用
举个例子,用户更改登录密码,是通过运行命令passwd来实现的。最终必须要修改/etc/passwd文件,而passwd的文件的属性是:
-
# stat /etc/passwd
-
文件:"/etc/passwd"
-
大小:1918 块:8 IO 块:4096 普通文件
-
设备:801h/2049d Inode:1076219 硬链接:1
-
权限:(0644/-rw-r--r--) Uid:( 0/ root) Gid:( 0/ root)
-
最近访问:2017-04-28 09:36:00.775552859 +0800
-
最近更改:2016-12-01 10:28:54.801942085 +0800
-
最近改动:2016-12-01 10:28:54.809942031 +0800
-
创建时间:-
我们可以看到 passwd 文件只有对于 root 用户是可写的,而对于所有的他用户来说都是没有写权限的。 那么一个普通的用户如何能够通过运行 passwd 命令修改这个 passwd 文件呢?为了解决这个问题,SUID/SGID 便应运而生。当一个程序设置了为SUID位时,内核就知道了运行这个程序的时候,应该认为是文件的所有者在运行这个程序。即该程序运行的时候,有效用户ID是该程序的所有者。
有效用户ID和有效组ID则决定了进程在运行时的权限。内核在决定进程是否有文件存取权限时,是采用了进程的有效用户ID来进行判断的。
虽然你以 tarena t登陆系统,但是当你输入 passwd 命令来更改密码的时候,由于 passwd 设置了 SUID 位,因此虽然进程的实际用户 ID 是 tarena 对应的 ID,但是进程的有效用户 ID 则是 passwd 文件的所有者 root 的 ID,因此可以修改 /etc/passwd 文件。
3、有效用户/组 ID 和设置用户/组/ ID 位关系
假设 test 文件的拥有这用户和组都是 root ,且其他用户对该文件可执行权限。
-
# ls -la test
-
-rwxr-xr-x 1 root root 7238 Apr 27 15:35 test
而此时我注销root用户,使用隶属于 tarena 组 (GID = 1000)和tarena 用户(UID = 1000)登录系统,运行 test 程序。
-
#include <stdio.h>
-
#include <unistd.h>
-
#include <sys/types.h>
-
-
int main (void)
-
{
-
printf ("uid = %d\n", getuid ());
-
printf ("gid = %d\n", getgid ());
-
printf ("euid = %d\n", geteuid ());
-
printf ("egid = %d\n", getegid ());
-
return 0;
-
}
-
输出结果:
-
uid = 1000
-
gid = 1000
-
euid = 1000
-
egid = 1000
可以看到进程的有效用户 ID 和实际用户 ID 一样都是 1000,而有效组 ID 也和实际组 ID一样都是 1000。
它们都是取自可执行文件的拥有者用户 ID 和(或)组 ID。
现在 root 用户为 test 文件添加设置用户 ID 位和设置组 ID 位权限位。也必须使用 sudo 普通用户无法修改权限。
-
设置权限
-
sudo chmod u+s test
-
sudo chmod g+s test
-
-
查看 test 权限
-
$ ls -la test
-
-rwsrwsr-x 1 root root 7314 4月 27 16:10 test
使用隶属于 tarena 组和 tarena用户再次运行 test 程序
-
#include <stdio.h>
-
#include <unistd.h>
-
#include <sys/types.h>
-
-
int main (void)
-
{
-
printf ("uid = %d\n", getuid ());
-
printf ("gid = %d\n", getgid ());
-
printf ("euid = %d\n", geteuid ());
-
printf ("egid = %d\n", getegid ());
-
return 0;
-
}
-
输出结果:
-
uid = 1000
-
gid = 1000
-
euid = 0
-
egid = 0
不难发现,进程的实际用户 ID 和实际组 ID 并没有发生变化,仍然是 1000,但它的有效用户 ID 和有效组 ID 却变成了 0,显然这是 test 文件的拥有者 root 用户 ID 和 组 ID,而参与权限判断,决定该进程能做什么不能做什么的恰恰是它的有效用户 ID 和有效组 ID,tarena 用户扮演 root 用户行使权限。
四、实际用户 ID和有效用户 ID 区别
上面我们有简单的介绍了下,实际用户 ID 和有效用户 ID的。
(1)实际用户ID和实际用户组ID
标识我是谁。也就是登录用户的uid和gid,比如我的 Linux 以 tarena 登录,在Linux运行的所有的命令的实际用户ID都是 tarena 用户组ID都是 tarena 的gid(可以用id命令查看)。
(2)有效用户ID和有效用户组ID
(2)有效用户ID和有效用户组ID
进程用来决定我们对资源的访问权限。一般情况下,有效用户ID等于实际用户ID,有效用户组ID等于实际用户组ID。当设置用户 ID(SUID)位设置,则有效用户ID等于文件的所有者的uid,而不是实际用户ID;同样,如果设置了设置用户组 ID(SGID)位,则有效用户组ID等于文件所有者的gid,而不是实际用户组ID。
这里介绍一个命令 id ,参看:id 命令
功能:
id 命令可以显示真实有效的用户 ID(UID)和组(ID)。
选项:
示例:
-g或--group 显示用户所属群组的ID。 -G或--groups 显示用户所属附加群组的ID。 -n或--name 显示用户,所属群组或附加群组的名称。 -r或--real 显示实际ID。 -u或--user 显示用户ID。 -help 显示帮助。 -version 显示版本信息。
-
//超级用户 root 登录
-
# id
-
uid=0(root) gid=0(root) 组=0(root)
-
//普通用户 tarena 登录
-
$ id
-
uid=1000(tarena) gid=1000(tarena) 组=1000(tarena),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare)
五、更改用户 ID 和更改组 ID
在 UNIX 系统中,特权(如能改变当前日期的表示法)以及访问控制(如能否读、写一个特定文件),是基于用户 ID 和组 ID 的。当程序需要增加特权,或需要访问当前并不允许访问的资源时,我们需要更换自己的用户 ID 和组 ID,使得新 ID 具有合适的特权或访问权限。与此类此,当程序需要降低其特权或阻止对某些资源的访问时,也需要更换用户 ID 或组 ID,新 ID 不具有相应特权或访问这些资源的能力。
1、我们可以用 setuid 函数设置实际用户 ID 和有效用户 ID。可以用 setgid 函数设置实际组 ID 和有效组 ID。
-
#include <sys/types.h>
-
#include <unistd.h>
-
int setuid(uid_t uid);
-
int setgid(gid_t gid);
-
两个函数返回值,若成功,返回 0,;若出错,返回-1
(1)函数解析
关于谁能更改 ID 有若干规则。现在先考虑更改用户 ID 的规则(关于用户 ID 我们所说明的一切都适用于组 ID)。1)若进程具有超级用户特权,则 setuid 函数将实际用户 ID、有效用户 ID 以及保存的设置用户 ID(SUID) 设置为 uid。
2)若进程没有超级用户特权,但是 uid 等于实际用户 ID 或保存的设置用户 ID,则 setuid 只将有效用户 ID 设置为 uid。不更改实际用户 ID 和保存的设置用户 ID。
3)如果上面两个条件都不满足,则 errno 设置为 EPERM,并返回 -1
在此假定 _POSIX_SAVED_IDS 为真。如果没有提供这种功能,则上面所说的关于保存的设置用户 ID 部分都无效。
关于内核所维护的 3 个用户 ID,还要注意以下几点。
1)只有超级用户进程可以更改实际用户 ID。通常,实际用户 ID 是在用户登录时,由 login 程序设置的,而且决不会改变它。因为 login 是一个超级用户进程,当它调用 setuid 时,设置所有 3 个用户 ID。
2)仅当对程序文件设置了用户 ID 位时,exec 函数才设置有效用户 ID。如果设置用户 ID 位没有设置,exec 函数不会改变有效用户 ID,而将维持其现有值。任何时候都可以调用 setuid ,将有效用户 ID 设置为实际用户 ID 或保存的设置用户 ID。自然地,不能将有效用户 ID 设置为任一随机值。
3)保存的设置用户 ID 是由 exec 复制有效用户 ID 而得到的。如果设置了文件的设置用户 ID 位,则在 exec 根据文件的用户 ID 设置了进程的有效 ID 以后,这个副本就被保存起来了。
总结,更改 3 个用户 ID 的不同方法:
(2)示例说明
-
#include <stdio.h>
-
#include <unistd.h>
-
#include <sys/types.h>
-
#include <stdlib.h>
-
-
int main (void)
-
{
-
printf ("uid = %d\n", getuid ());
-
printf ("gid = %d\n", getgid ());
-
printf ("euid = %d\n", geteuid ());
-
printf ("egid = %d\n", getegid ());
-
if(!setgid(1234))
-
printf("setgid successfully!\n");
-
else
-
perror ("setgid"), exit (1);
-
-
if(!setuid(1234))
-
printf("setuid successfully!\n");
-
else
-
perror ("setuid"), exit (1);
-
-
printf ("--------------------\n");
-
printf ("uid = %d\n", getuid ());
-
printf ("gid = %d\n", getgid ());
-
printf ("euid = %d\n", geteuid ());
-
printf ("egid = %d\n", getegid ());
-
return 0;
-
}
-
输出结果:
-
uid = 0
-
gid = 0
-
euid = 0
-
egid = 0
-
setgid successfully!
-
setuid successfully!
-
--------------------
-
uid = 1234
-
gid = 1234
-
euid = 1234
-
egid = 1234
考虑一个问题,为什么我先使用 setgid,然后再使用 setuid?
如果反过来会出现 setgid: Operation not permitted。
2、函数 seteuid 和 setegit
-
#include <unistd.h>
-
int seteuid(uid_t euid);
-
int setegid(gid_t egid);
-
两个函数返回值,若成功,返回 0,失败返回 -1
(1)函数解析
它们类似于 setuid 和 setgid,但是只更改有效用户 ID 和有效组 ID。
一个非特权用户可将其有效用户 ID 设置为实际用户 ID 或保存设置用户 ID。对于一个特权用户则可将有效用户 ID 设置为 uid。这区别于 setuid 函数,它更改所有 3 个用户 ID。
(2)示例说明
-
#include <stdio.h>
-
#include <unistd.h>
-
#include <sys/types.h>
-
#include <stdlib.h>
-
-
int main (void)
-
{
-
printf ("uid = %d\n", getuid ());
-
printf ("gid = %d\n", getgid ());
-
printf ("euid = %d\n", geteuid ());
-
printf ("egid = %d\n", getegid ());
-
if(!setegid(1234))
-
printf("setegid successfully!\n");
-
else
-
perror ("setegid"), exit (1);
-
-
if(!seteuid(1234))
-
printf("seteuid successfully!\n");
-
else
-
perror ("seteuid"), exit (1);
-
-
printf ("--------------------\n");
-
printf ("uid = %d\n", getuid ());
-
printf ("gid = %d\n", getgid ());
-
printf ("euid = %d\n", geteuid ());
-
printf ("egid = %d\n", getegid ());
-
return 0;
-
}
-
输出结果:
-
uid = 0
-
gid = 0
-
euid = 0
-
egid = 0
-
setegid successfully!
-
seteuid successfully!
-
--------------------
-
uid = 0
-
gid = 0
-
euid = 1234
-
egid = 1234
-
#include <sys/types.h>
-
#include <unistd.h>
-
int setreuid(uid_t ruid, uid_t euid);
-
int setregid(gid_t rgid, gid_t egid);
-
两个函数返回值,若成功,返回 0;若出错,返回 -1
(1)函数解析
函数功能是交换实际用户 ID 和有效用户 ID 的值。
如若其中任一参数的值为 -1,则表示相应的 ID 保持不变。
规则很简单:一个非特权用户总能交换实际用户 ID 和有效 ID。这就允许一个设置用户 ID 程序交换成用户的普通权限,以后又可再次交换用户 ID 权限。POSIX.1 引进了保存的设置用户 ID 特性后,其规则也相应的加强,它允许了一个非特权用户将其有特权用户 ID 设置为 保存的设置用户 ID。
(2)示例说明
-
#include <stdio.h>
-
#include <unistd.h>
-
#include <sys/types.h>
-
#include <stdlib.h>
-
-
int main (void)
-
{
-
printf ("uid = %d\n", getuid ());
-
printf ("gid = %d\n", getgid ());
-
printf ("euid = %d\n", geteuid ());
-
printf ("egid = %d\n", getegid ());
-
if(!setregid(0, 1234))
-
printf("setregid successfully!\n");
-
else
-
perror ("setregid"), exit (1);
-
-
if(!setreuid(0, 1234))
-
printf("setreuid successfully!\n");
-
else
-
perror ("setreuid"), exit (1);
-
-
printf ("--------------------\n");
-
printf ("uid = %d\n", getuid ());
-
printf ("gid = %d\n", getgid ());
-
printf ("euid = %d\n", geteuid ());
-
printf ("egid = %d\n", getegid ());
-
return 0;
-
}
-
输出结果:
-
uid = 0
-
gid = 0
-
euid = 0
-
egid = 0
-
setregid successfully!
-
setreuid successfully!
-
--------------------
-
uid = 0
-
gid = 0
-
euid = 1234
-
egid = 1234
4、设置不同用户 ID 的各个函数关系
函数 waitid 未讲
函数 wait3 和 wait4 未讲
竞争条件 未讲
解释器文件 未讲 (后续添加到,shell编程这篇文章里,参看:UNIX再学习 -- shell编程)
进程会计 未讲
用户标识 未讲
进程调度 未讲
进程时间 未讲 (这部分查看时间函数,参看:C语言再学习 -- 时间函数)
上一篇: vue实现点击切换不同的页面
下一篇: 022 UNIX再学习 -- 信号处理